diff --git a/app.py b/app.py index 1717daa..7b82cfb 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,7 @@ from flask import Flask, render_template, request, jsonify, url_for from flask_sock import Sock from lib.config_manager import ConfigurationManager +from lib.launcher_manager import LauncherManager import os import json # Added import from datetime import datetime @@ -22,6 +23,9 @@ app = Flask( sock = Sock(app) config_manager = ConfigurationManager() +# Inicializar launcher manager +launcher_manager = LauncherManager(config_manager.data_path) + # Lista global para mantener las conexiones WebSocket activas websocket_connections = set() @@ -509,6 +513,151 @@ def exit_application(icon, item): tray_icon.stop() +# === LAUNCHER GUI APIs === + +@app.route("/api/launcher-groups", methods=["GET", "POST"]) +def handle_launcher_groups(): + """Gestionar grupos de launcher (GET: obtener, POST: crear)""" + if request.method == "GET": + try: + groups = launcher_manager.get_launcher_groups() + return jsonify(groups) + except Exception as e: + return jsonify({"error": str(e)}), 500 + else: # POST + try: + data = request.json + result = launcher_manager.add_launcher_group(data) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/launcher-groups/", methods=["GET", "PUT", "DELETE"]) +def handle_launcher_group(group_id): + """Gestionar grupo específico (GET: obtener, PUT: actualizar, DELETE: eliminar)""" + if request.method == "GET": + try: + group = launcher_manager.get_launcher_group(group_id) + if not group: + return jsonify({"error": "Group not found"}), 404 + return jsonify(group) + except Exception as e: + return jsonify({"error": str(e)}), 500 + elif request.method == "PUT": + try: + data = request.json + result = launcher_manager.update_launcher_group(group_id, data) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + else: # DELETE + try: + result = launcher_manager.delete_launcher_group(group_id) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/launcher-scripts/") +def get_launcher_scripts(group_id): + """Obtener scripts de un grupo del launcher""" + try: + scripts = launcher_manager.get_group_scripts(group_id) + return jsonify(scripts) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/execute-gui-script", methods=["POST"]) +def execute_gui_script(): + """Ejecutar script GUI con argumentos opcionales""" + try: + data = request.json + group_id = data["group_id"] + script_name = data["script_name"] + script_args = data.get("args", []) + + result = launcher_manager.execute_gui_script( + group_id, script_name, script_args, broadcast_message + ) + return jsonify(result) + except Exception as e: + error_msg = f"Error ejecutando script GUI: {str(e)}" + broadcast_message(error_msg) + return jsonify({"error": error_msg}), 500 + +@app.route("/api/launcher-favorites", methods=["GET", "POST"]) +def handle_launcher_favorites(): + """Gestionar favoritos del launcher""" + if request.method == "GET": + try: + favorites = launcher_manager.get_favorites() + return jsonify({"favorites": favorites}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + else: # POST + try: + data = request.json + group_id = data["group_id"] + script_name = data["script_name"] + result = launcher_manager.toggle_favorite(group_id, script_name) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/launcher-history", methods=["GET", "DELETE"]) +def handle_launcher_history(): + """Gestionar historial del launcher""" + if request.method == "GET": + try: + history = launcher_manager.get_history() + return jsonify({"history": history}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + else: # DELETE + try: + result = launcher_manager.clear_history() + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/launcher-categories") +def get_launcher_categories(): + """Obtener categorías disponibles del launcher""" + try: + categories = launcher_manager.get_categories() + return jsonify(categories) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/group-icon//") +def get_group_icon(launcher_type, group_id): + """Obtener icono de un grupo (config o launcher)""" + try: + if launcher_type == "launcher": + group = launcher_manager.get_launcher_group(group_id) + if not group: + return jsonify({"error": "Group not found"}), 404 + + icon_path = os.path.join(group["directory"], "icon.ico") + if os.path.exists(icon_path): + from flask import send_file + return send_file(icon_path, mimetype='image/x-icon') + + elif launcher_type == "config": + group_path = os.path.join(config_manager.script_groups_path, group_id) + icon_path = os.path.join(group_path, "icon.ico") + if os.path.exists(icon_path): + from flask import send_file + return send_file(icon_path, mimetype='image/x-icon') + + # Icono por defecto - devolver datos para que el frontend genere el icono + return jsonify({"type": "default", "icon": "📁"}) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# === FIN LAUNCHER GUI APIs === + + if __name__ == "__main__": # --- Start Flask in a background thread --- flask_thread = threading.Thread(target=run_flask, daemon=True) diff --git a/data/launcher_favorites.json b/data/launcher_favorites.json new file mode 100644 index 0000000..dce42c1 --- /dev/null +++ b/data/launcher_favorites.json @@ -0,0 +1,12 @@ +{ + "favorites": [ + { + "id": "1_calc.py", + "group_id": "1", + "script_name": "calc.py", + "added_date": "2025-06-03T11:47:24.757186Z", + "execution_count": 0, + "last_executed": null + } + ] +} \ No newline at end of file diff --git a/data/launcher_history.json b/data/launcher_history.json new file mode 100644 index 0000000..f0b2d23 --- /dev/null +++ b/data/launcher_history.json @@ -0,0 +1,9 @@ +{ + "history": [], + "settings": { + "max_entries": 100, + "auto_cleanup_days": 30, + "track_execution_time": true, + "track_arguments": true + } +} \ No newline at end of file diff --git a/data/launcher_scripts.json b/data/launcher_scripts.json new file mode 100644 index 0000000..8b2af1c --- /dev/null +++ b/data/launcher_scripts.json @@ -0,0 +1,77 @@ +{ + "version": "1.0", + "groups": [ + { + "id": "1", + "name": "Calculadora", + "description": "", + "category": "Herramientas", + "version": "1.0", + "directory": "D:/Proyectos/Scripts/Calcv2", + "author": "", + "tags": [], + "created_date": "2025-06-03T11:46:28.443910Z", + "updated_date": "2025-06-03T11:46:28.443910Z" + } + ], + "categories": { + "Herramientas": { + "color": "#3B82F6", + "icon": "🔧", + "subcategories": [ + "Generales", + "Desarrollo", + "Sistema" + ] + }, + "Análisis": { + "color": "#10B981", + "icon": "📊", + "subcategories": [ + "Datos", + "Estadísticas", + "Visualización" + ] + }, + "Utilidades": { + "color": "#8B5CF6", + "icon": "⚙️", + "subcategories": [ + "Archivos", + "Texto", + "Conversión" + ] + }, + "Desarrollo": { + "color": "#F59E0B", + "icon": "💻", + "subcategories": [ + "Code", + "Testing", + "Deploy" + ] + }, + "Visualización": { + "color": "#EF4444", + "icon": "📈", + "subcategories": [ + "Gráficos", + "Reportes", + "Dashboard" + ] + }, + "Otros": { + "color": "#6B7280", + "icon": "📁", + "subcategories": [ + "Misceláneos" + ] + } + }, + "settings": { + "default_execution_directory": "script_directory", + "enable_argument_validation": true, + "max_history_entries": 100, + "auto_cleanup_days": 30 + } +} \ No newline at end of file diff --git a/data/log.txt b/data/log.txt index 61fa4c7..e69de29 100644 --- a/data/log.txt +++ b/data/log.txt @@ -1,23 +0,0 @@ -[12:38:21] Iniciando ejecución de x7_value_updater.py en C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001... -[12:38:21] Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 -[12:38:21] Los archivos JSON se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json -[12:38:21] Los archivos de documentación se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation -[12:38:21] Se encontraron 1 pares de archivos para procesar. -[12:38:21] --- Procesando par de archivos --- -[12:38:21] Data file: DB1001_data.AWL -[12:38:21] Format file: DB1001_format.AWL -[12:38:21] Parseando archivo data: DB1001_data.AWL -[12:38:21] Parseando archivo format: DB1001_format.AWL -[12:38:21] Archivos JSON generados: DB1001_data.json y DB1001_format.json -[12:38:21] Comparando estructuras para DB 'HMI_Blender_Parameters': 284 variables en _data, 284 variables en _format -[12:38:21] Los archivos son compatibles. Creando el archivo _updated... -[12:38:21] Procesando DB 'HMI_Blender_Parameters': 284 variables en _format, 284 variables en _data -[12:38:21] Estadísticas para DB 'HMI_Blender_Parameters': 280 variables actualizadas, 0 no encontradas -[12:38:21] Archivo _updated generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\DB1001_updated.json -[12:38:22] Archivo Excel de comparación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_comparison.xlsx -[12:38:22] Archivo Markdown generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_updated.md -[12:38:22] Archivo S7 generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_updated.txt -[12:38:22] Archivo S7 copiado a: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\DB1001_updated.AWL -[12:38:22] --- Proceso completado --- -[12:38:23] Ejecución de x7_value_updater.py finalizada (success). Duración: 0:00:01.539943. -[12:38:23] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\S7_DB_Utils\log_x7_value_updater.txt diff --git a/lib/launcher_manager.py b/lib/launcher_manager.py new file mode 100644 index 0000000..3e5c5c9 --- /dev/null +++ b/lib/launcher_manager.py @@ -0,0 +1,416 @@ +import os +import json +import subprocess +import sys +from typing import Dict, Any, List, Optional +from datetime import datetime +import uuid + +class LauncherManager: + def __init__(self, data_path: str): + self.data_path = data_path + self.launcher_config_path = os.path.join(data_path, "launcher_scripts.json") + self.favorites_path = os.path.join(data_path, "launcher_favorites.json") + self.history_path = os.path.join(data_path, "launcher_history.json") + + # Inicializar archivos si no existen + self._initialize_files() + + def _initialize_files(self): + """Crear archivos de configuración por defecto si no existen""" + # Inicializar launcher_scripts.json + if not os.path.exists(self.launcher_config_path): + default_config = { + "version": "1.0", + "groups": [], + "categories": { + "Herramientas": { + "color": "#3B82F6", + "icon": "🔧", + "subcategories": ["Generales", "Desarrollo", "Sistema"] + }, + "Análisis": { + "color": "#10B981", + "icon": "📊", + "subcategories": ["Datos", "Estadísticas", "Visualización"] + }, + "Utilidades": { + "color": "#8B5CF6", + "icon": "⚙️", + "subcategories": ["Archivos", "Texto", "Conversión"] + }, + "Desarrollo": { + "color": "#F59E0B", + "icon": "💻", + "subcategories": ["Code", "Testing", "Deploy"] + }, + "Visualización": { + "color": "#EF4444", + "icon": "📈", + "subcategories": ["Gráficos", "Reportes", "Dashboard"] + }, + "Otros": { + "color": "#6B7280", + "icon": "📁", + "subcategories": ["Misceláneos"] + } + }, + "settings": { + "default_execution_directory": "script_directory", + "enable_argument_validation": True, + "max_history_entries": 100, + "auto_cleanup_days": 30 + } + } + with open(self.launcher_config_path, 'w', encoding='utf-8') as f: + json.dump(default_config, f, indent=2, ensure_ascii=False) + + # Inicializar launcher_favorites.json + if not os.path.exists(self.favorites_path): + default_favorites = {"favorites": []} + with open(self.favorites_path, 'w', encoding='utf-8') as f: + json.dump(default_favorites, f, indent=2, ensure_ascii=False) + + # Inicializar launcher_history.json + if not os.path.exists(self.history_path): + default_history = { + "history": [], + "settings": { + "max_entries": 100, + "auto_cleanup_days": 30, + "track_execution_time": True, + "track_arguments": True + } + } + with open(self.history_path, 'w', encoding='utf-8') as f: + json.dump(default_history, f, indent=2, ensure_ascii=False) + + def get_launcher_groups(self) -> List[Dict[str, Any]]: + """Obtener todos los grupos de scripts GUI""" + try: + with open(self.launcher_config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + return config.get("groups", []) + except Exception as e: + print(f"Error loading launcher groups: {e}") + return [] + + def get_launcher_group(self, group_id: str) -> Optional[Dict[str, Any]]: + """Obtener un grupo específico por ID""" + groups = self.get_launcher_groups() + for group in groups: + if group.get("id") == group_id: + return group + return None + + def add_launcher_group(self, group_data: Dict[str, Any]) -> Dict[str, str]: + """Agregar nuevo grupo de scripts GUI""" + try: + # Validar datos requeridos + required_fields = ["name", "directory"] + for field in required_fields: + if not group_data.get(field): + return {"status": "error", "message": f"Campo requerido: {field}"} + + # Validar que el directorio existe + if not os.path.isdir(group_data["directory"]): + return {"status": "error", "message": "El directorio especificado no existe"} + + # Generar ID único si no se proporciona + if not group_data.get("id"): + group_data["id"] = str(uuid.uuid4())[:8] + + # Verificar que el ID no exista + if self.get_launcher_group(group_data["id"]): + return {"status": "error", "message": "Ya existe un grupo con este ID"} + + # Agregar campos por defecto + current_time = datetime.now().isoformat() + "Z" + group_data.setdefault("description", "") + group_data.setdefault("category", "Otros") + group_data.setdefault("version", "1.0") + group_data.setdefault("author", "") + group_data.setdefault("tags", []) + group_data.setdefault("created_date", current_time) + group_data["updated_date"] = current_time + + # Cargar configuración y agregar grupo + with open(self.launcher_config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + config["groups"].append(group_data) + + with open(self.launcher_config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + return {"status": "success", "message": "Grupo agregado exitosamente"} + + except Exception as e: + return {"status": "error", "message": f"Error agregando grupo: {str(e)}"} + + def update_launcher_group(self, group_id: str, group_data: Dict[str, Any]) -> Dict[str, str]: + """Actualizar grupo existente""" + try: + with open(self.launcher_config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + # Buscar y actualizar el grupo + group_found = False + for i, group in enumerate(config["groups"]): + if group["id"] == group_id: + # Mantener ID y fechas de creación + group_data["id"] = group_id + group_data["created_date"] = group.get("created_date", datetime.now().isoformat() + "Z") + group_data["updated_date"] = datetime.now().isoformat() + "Z" + + config["groups"][i] = group_data + group_found = True + break + + if not group_found: + return {"status": "error", "message": "Grupo no encontrado"} + + with open(self.launcher_config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + return {"status": "success", "message": "Grupo actualizado exitosamente"} + + except Exception as e: + return {"status": "error", "message": f"Error actualizando grupo: {str(e)}"} + + def delete_launcher_group(self, group_id: str) -> Dict[str, str]: + """Eliminar grupo de scripts GUI""" + try: + with open(self.launcher_config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + # Filtrar el grupo a eliminar + original_count = len(config["groups"]) + config["groups"] = [g for g in config["groups"] if g["id"] != group_id] + + if len(config["groups"]) == original_count: + return {"status": "error", "message": "Grupo no encontrado"} + + with open(self.launcher_config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + # Limpiar favoritos del grupo eliminado + self._cleanup_favorites_for_group(group_id) + + return {"status": "success", "message": "Grupo eliminado exitosamente"} + + except Exception as e: + return {"status": "error", "message": f"Error eliminando grupo: {str(e)}"} + + def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: + """Obtener scripts de un grupo específico""" + try: + group = self.get_launcher_group(group_id) + if not group: + return [] + + directory = group["directory"] + if not os.path.isdir(directory): + return [] + + scripts = [] + for file in os.listdir(directory): + if file.endswith('.py') and not file.startswith('_'): + script_path = os.path.join(directory, file) + if os.path.isfile(script_path): + scripts.append({ + "name": file, + "display_name": file[:-3], # Sin extensión .py + "path": script_path, + "size": os.path.getsize(script_path), + "modified": datetime.fromtimestamp(os.path.getmtime(script_path)).isoformat() + }) + + return sorted(scripts, key=lambda x: x["name"]) + + except Exception as e: + print(f"Error getting scripts for group {group_id}: {e}") + return [] + + def execute_gui_script(self, group_id: str, script_name: str, script_args: List[str], + broadcast_func) -> Dict[str, Any]: + """Ejecutar script GUI con argumentos opcionales""" + try: + group = self.get_launcher_group(group_id) + if not group: + return {"status": "error", "message": "Grupo no encontrado"} + + script_path = os.path.join(group["directory"], script_name) + if not os.path.isfile(script_path): + return {"status": "error", "message": "Script no encontrado"} + + # Construir comando + cmd = [sys.executable, script_path] + script_args + working_dir = group["directory"] # Por defecto directorio del script + + broadcast_func(f"Ejecutando script GUI: {script_name}") + broadcast_func(f"Comando: {' '.join(cmd)}") + broadcast_func(f"Directorio: {working_dir}") + + # Ejecutar script + start_time = datetime.now() + process = subprocess.Popen( + cmd, + cwd=working_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0 + ) + + # Registrar en historial + execution_id = str(uuid.uuid4())[:8] + self._add_to_history({ + "id": execution_id, + "group_id": group_id, + "script_name": script_name, + "executed_date": start_time.isoformat() + "Z", + "arguments": script_args, + "working_directory": working_dir, + "status": "running", + "pid": process.pid + }) + + broadcast_func(f"Script GUI ejecutado con PID: {process.pid}") + broadcast_func(f"ID de ejecución: {execution_id}") + + return { + "status": "success", + "message": "Script GUI ejecutado exitosamente", + "execution_id": execution_id, + "pid": process.pid + } + + except Exception as e: + error_msg = f"Error ejecutando script GUI: {str(e)}" + broadcast_func(error_msg) + return {"status": "error", "message": error_msg} + + def get_favorites(self) -> List[Dict[str, Any]]: + """Obtener lista de favoritos""" + try: + with open(self.favorites_path, 'r', encoding='utf-8') as f: + data = json.load(f) + return data.get("favorites", []) + except Exception as e: + print(f"Error loading favorites: {e}") + return [] + + def toggle_favorite(self, group_id: str, script_name: str) -> Dict[str, str]: + """Agregar o quitar script de favoritos""" + try: + with open(self.favorites_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + favorites = data.get("favorites", []) + favorite_id = f"{group_id}_{script_name}" + + # Buscar si ya existe + existing_favorite = None + for i, fav in enumerate(favorites): + if fav["group_id"] == group_id and fav["script_name"] == script_name: + existing_favorite = i + break + + if existing_favorite is not None: + # Quitar de favoritos + favorites.pop(existing_favorite) + action = "removed" + else: + # Agregar a favoritos + favorites.append({ + "id": favorite_id, + "group_id": group_id, + "script_name": script_name, + "added_date": datetime.now().isoformat() + "Z", + "execution_count": 0, + "last_executed": None + }) + action = "added" + + data["favorites"] = favorites + with open(self.favorites_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + return {"status": "success", "action": action} + + except Exception as e: + return {"status": "error", "message": f"Error managing favorite: {str(e)}"} + + def get_history(self) -> List[Dict[str, Any]]: + """Obtener historial de ejecución""" + try: + with open(self.history_path, 'r', encoding='utf-8') as f: + data = json.load(f) + return data.get("history", []) + except Exception as e: + print(f"Error loading history: {e}") + return [] + + def clear_history(self) -> Dict[str, str]: + """Limpiar historial de ejecución""" + try: + with open(self.history_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + data["history"] = [] + + with open(self.history_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + return {"status": "success", "message": "Historial limpiado exitosamente"} + + except Exception as e: + return {"status": "error", "message": f"Error clearing history: {str(e)}"} + + def get_categories(self) -> Dict[str, Any]: + """Obtener categorías disponibles""" + try: + with open(self.launcher_config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + return config.get("categories", {}) + except Exception as e: + print(f"Error loading categories: {e}") + return {} + + def _add_to_history(self, entry: Dict[str, Any]): + """Agregar entrada al historial""" + try: + with open(self.history_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + history = data.get("history", []) + history.insert(0, entry) # Agregar al inicio + + # Limitar tamaño del historial + max_entries = data.get("settings", {}).get("max_entries", 100) + if len(history) > max_entries: + history = history[:max_entries] + + data["history"] = history + + with open(self.history_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + except Exception as e: + print(f"Error adding to history: {e}") + + def _cleanup_favorites_for_group(self, group_id: str): + """Limpiar favoritos de un grupo eliminado""" + try: + with open(self.favorites_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # Filtrar favoritos del grupo eliminado + data["favorites"] = [f for f in data.get("favorites", []) if f.get("group_id") != group_id] + + with open(self.favorites_path, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + except Exception as e: + print(f"Error cleaning up favorites for group {group_id}: {e}") \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index 9bf4189..a17a408 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -5,7 +5,7 @@ width: 400px; height: 100vh; background: white; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); + box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); transition: right 0.3s ease; z-index: 40; overflow-y: auto; @@ -21,7 +21,7 @@ left: 0; width: 100vw; height: 100vh; - background: rgba(0,0,0,0.5); + background: rgba(0, 0, 0, 0.5); display: none; z-index: 30; } @@ -106,23 +106,29 @@ /* Estilos para encabezados dentro de la descripción larga del script */ .long-description-content h1 { - font-size: 1.875rem; /* Equivalente a text-3xl de Tailwind */ + font-size: 1.875rem; + /* Equivalente a text-3xl de Tailwind */ font-weight: bold; margin-top: 1rem; margin-bottom: 0.5rem; } + .long-description-content h2 { - font-size: 1.5rem; /* Equivalente a text-2xl */ + font-size: 1.5rem; + /* Equivalente a text-2xl */ font-weight: bold; margin-top: 0.875rem; margin-bottom: 0.4rem; } + .long-description-content h3 { - font-size: 1.25rem; /* Equivalente a text-xl */ + font-size: 1.25rem; + /* Equivalente a text-xl */ font-weight: bold; margin-top: 0.75rem; margin-bottom: 0.3rem; } + /* Puedes añadir estilos para h4, h5, h6 si los necesitas */ .long-description-content hr { @@ -139,26 +145,392 @@ margin-top: 0.5rem; margin-bottom: 0.5rem; } + .long-description-content ul { list-style-type: disc; } + .long-description-content ol { list-style-type: decimal; } + .long-description-content pre { - background-color: #f3f4f6; /* bg-gray-100 */ + background-color: #f3f4f6; + /* bg-gray-100 */ padding: 0.75rem; border-radius: 0.25rem; overflow-x: auto; margin-top: 0.5rem; margin-bottom: 0.5rem; } + .long-description-content code { font-family: monospace; /* Estilo para código en línea si es necesario */ } + /* Estilo específico para bloques de código dentro de
 */
 .long-description-content pre code {
     background-color: transparent;
     padding: 0;
+}
+
+/* ===== LAUNCHER GUI STYLES ===== */
+
+/* Tab Styles */
+.tab-button {
+    color: #6B7280;
+    border-color: transparent;
+    transition: all 0.2s ease;
+}
+
+.tab-button:hover {
+    color: #374151;
+    border-color: #D1D5DB;
+}
+
+.tab-button.active {
+    color: #3B82F6;
+    border-color: #3B82F6;
+}
+
+.tab-content {
+    display: block;
+}
+
+.tab-content.hidden {
+    display: none;
+}
+
+/* Icon Styles */
+.group-icon {
+    width: 48px;
+    height: 48px;
+    border-radius: 8px;
+    object-fit: cover;
+    border: 2px solid #E5E7EB;
+}
+
+.group-icon.default {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    font-size: 20px;
+}
+
+.group-icon-small {
+    width: 24px;
+    height: 24px;
+    border-radius: 4px;
+    object-fit: cover;
+    border: 1px solid #E5E7EB;
+}
+
+.group-icon-small.default {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    font-size: 12px;
+}
+
+/* Category Styles */
+.category-badge {
+    display: inline-block;
+    padding: 0.25rem 0.5rem;
+    background: #EFF6FF;
+    color: #1D4ED8;
+    border-radius: 9999px;
+    font-size: 0.75rem;
+    font-weight: 500;
+}
+
+.category-btn {
+    background: #F9FAFB;
+    color: #6B7280;
+    border-color: #E5E7EB;
+    transition: all 0.2s ease;
+}
+
+.category-btn:hover {
+    background: #F3F4F6;
+    color: #374151;
+    border-color: #D1D5DB;
+}
+
+.category-btn.active {
+    background: #3B82F6;
+    color: white;
+    border-color: #3B82F6;
+}
+
+/* Script Card Styles */
+.script-card {
+    border: 1px solid #E5E7EB;
+    border-radius: 8px;
+    padding: 1rem;
+    background: white;
+    transition: all 0.2s ease;
+    position: relative;
+}
+
+.script-card:hover {
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+    border-color: #D1D5DB;
+}
+
+.script-card.favorited {
+    border-color: #FCD34D;
+    background: #FFFBEB;
+}
+
+/* Favorite Star */
+.favorite-star {
+    cursor: pointer;
+    transition: all 0.2s ease;
+    color: #D1D5DB;
+}
+
+.favorite-star:hover {
+    transform: scale(1.1);
+    color: #F59E0B;
+}
+
+.favorite-star.active {
+    color: #F59E0B;
+}
+
+/* History Item */
+.history-item {
+    border: 1px solid #E5E7EB;
+    border-radius: 6px;
+    padding: 0.75rem;
+    background: #F9FAFB;
+    transition: all 0.2s ease;
+}
+
+.history-item:hover {
+    background: #F3F4F6;
+    border-color: #D1D5DB;
+}
+
+.history-item.success {
+    border-left: 4px solid #10B981;
+}
+
+.history-item.error {
+    border-left: 4px solid #EF4444;
+}
+
+.history-item.running {
+    border-left: 4px solid #3B82F6;
+}
+
+/* Favorites Panel */
+.favorites-panel {
+    background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%);
+}
+
+.favorites-panel.empty {
+    display: none;
+}
+
+/* Group List Item */
+.group-list-item {
+    display: flex;
+    align-items: center;
+    padding: 0.5rem;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+}
+
+.group-list-item:hover {
+    background: #F3F4F6;
+}
+
+.group-list-item.selected {
+    background: #EBF8FF;
+    border: 1px solid #3B82F6;
+}
+
+.group-list-item .group-info {
+    margin-left: 0.75rem;
+    flex: 1;
+}
+
+.group-list-item .group-name {
+    font-weight: 500;
+    color: #374151;
+}
+
+.group-list-item .group-category {
+    font-size: 0.75rem;
+    color: #6B7280;
+}
+
+/* Modal Improvements */
+.modal-content {
+    max-width: 90vw;
+    max-height: 90vh;
+}
+
+.modal-header {
+    border-bottom: 1px solid #E5E7EB;
+    padding: 1.5rem;
+}
+
+.modal-footer {
+    border-top: 1px solid #E5E7EB;
+    padding: 1.5rem;
+}
+
+/* Script Grid */
+.scripts-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+    gap: 1rem;
+}
+
+/* Tooltip */
+.tooltip {
+    position: relative;
+}
+
+.tooltip:hover::after {
+    content: attr(data-tooltip);
+    position: absolute;
+    bottom: 100%;
+    left: 50%;
+    transform: translateX(-50%);
+    background: #374151;
+    color: white;
+    padding: 0.5rem;
+    border-radius: 4px;
+    white-space: nowrap;
+    z-index: 1000;
+    font-size: 0.75rem;
+}
+
+/* Loading States */
+.loading {
+    opacity: 0.6;
+    pointer-events: none;
+}
+
+.spinner {
+    display: inline-block;
+    width: 1rem;
+    height: 1rem;
+    border: 2px solid #E5E7EB;
+    border-radius: 50%;
+    border-top-color: #3B82F6;
+    animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
+/* Responsive Adjustments */
+@media (max-width: 768px) {
+    .scripts-grid {
+        grid-template-columns: 1fr;
+    }
+
+    .tab-button span {
+        font-size: 0.75rem;
+    }
+
+    .tab-button svg {
+        width: 1rem;
+        height: 1rem;
+    }
+}
+
+/* Animation Classes */
+.fade-in {
+    animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(10px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.slide-in {
+    animation: slideIn 0.3s ease-in-out;
+}
+
+@keyframes slideIn {
+    from {
+        opacity: 0;
+        transform: translateX(-20px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translateX(0);
+    }
+}
+
+/* Status Indicators */
+.status-success {
+    color: #10B981;
+}
+
+.status-error {
+    color: #EF4444;
+}
+
+.status-running {
+    color: #3B82F6;
+}
+
+.status-warning {
+    color: #F59E0B;
+}
+
+/* Empty State */
+.empty-state {
+    text-align: center;
+    padding: 3rem 1rem;
+    color: #6B7280;
+}
+
+.empty-state svg {
+    width: 4rem;
+    height: 4rem;
+    margin: 0 auto 1rem;
+    color: #D1D5DB;
+}
+
+/* Button Variants */
+.btn-outline {
+    border: 1px solid currentColor;
+    background: transparent;
+    transition: all 0.2s ease;
+}
+
+.btn-outline:hover {
+    background: currentColor;
+    color: white;
+}
+
+/* Focus States */
+.focus-visible:focus {
+    outline: 2px solid #3B82F6;
+    outline-offset: 2px;
 }
\ No newline at end of file
diff --git a/static/js/launcher.js b/static/js/launcher.js
new file mode 100644
index 0000000..8477f61
--- /dev/null
+++ b/static/js/launcher.js
@@ -0,0 +1,709 @@
+// launcher.js - Funcionalidad del Launcher GUI
+
+class LauncherManager {
+    constructor() {
+        this.currentGroup = null;
+        this.groups = [];
+        this.scripts = [];
+        this.favorites = new Set();
+        this.history = [];
+        this.categories = {};
+        this.currentFilter = 'all';
+        this.currentEditingGroup = null;
+    }
+
+    async init() {
+        console.log('Inicializando Launcher GUI...');
+        await this.loadCategories();
+        await this.loadGroups();
+        await this.loadFavorites();
+        await this.loadHistory();
+        this.setupEventListeners();
+        this.renderInterface();
+    }
+
+    async loadCategories() {
+        try {
+            const response = await fetch('/api/launcher-categories');
+            this.categories = await response.json();
+        } catch (error) {
+            console.error('Error loading launcher categories:', error);
+        }
+    }
+
+    async loadGroups() {
+        try {
+            const response = await fetch('/api/launcher-groups');
+            this.groups = await response.json();
+            this.renderGroupSelector();
+        } catch (error) {
+            console.error('Error loading launcher groups:', error);
+        }
+    }
+
+    async loadFavorites() {
+        try {
+            const response = await fetch('/api/launcher-favorites');
+            const data = await response.json();
+            this.favorites = new Set(data.favorites.map(f => `${f.group_id}_${f.script_name}`));
+            this.renderFavorites(data.favorites);
+        } catch (error) {
+            console.error('Error loading favorites:', error);
+        }
+    }
+
+    async loadHistory() {
+        try {
+            const response = await fetch('/api/launcher-history');
+            const data = await response.json();
+            this.history = data.history || [];
+            this.renderHistory();
+        } catch (error) {
+            console.error('Error loading history:', error);
+        }
+    }
+
+    setupEventListeners() {
+        // Event listener para el formulario de grupos
+        const groupForm = document.getElementById('group-form');
+        if (groupForm) {
+            groupForm.addEventListener('submit', (e) => {
+                e.preventDefault();
+                this.saveGroup();
+            });
+        }
+    }
+
+    renderInterface() {
+        this.renderGroupSelector();
+        this.renderCategoryFilter();
+        this.updateFavoritesCount();
+    }
+
+    renderGroupSelector() {
+        const selector = document.getElementById('launcher-group-select');
+        if (!selector) return;
+
+        selector.innerHTML = '';
+
+        this.groups.forEach(group => {
+            const option = document.createElement('option');
+            option.value = group.id;
+            option.textContent = group.name;
+            option.dataset.category = group.category;
+            option.dataset.description = group.description;
+            selector.appendChild(option);
+        });
+    }
+
+    renderCategoryFilter() {
+        const filterContainer = document.querySelector('.category-filter .flex');
+        if (!filterContainer) return;
+
+        // Limpiar botones existentes excepto "Todas"
+        const buttons = filterContainer.querySelectorAll('.category-btn:not([data-category="all"])');
+        buttons.forEach(btn => btn.remove());
+
+        // Agregar botones por categoría
+        Object.keys(this.categories).forEach(category => {
+            const categoryData = this.categories[category];
+            const button = document.createElement('button');
+            button.className = 'category-btn px-3 py-1 rounded-full text-sm border';
+            button.dataset.category = category;
+            button.innerHTML = `${categoryData.icon} ${category}`;
+            button.onclick = () => this.filterByCategory(category);
+            filterContainer.appendChild(button);
+        });
+    }
+
+    async loadLauncherScripts() {
+        const groupId = document.getElementById('launcher-group-select').value;
+        if (!groupId) {
+            this.scripts = [];
+            this.renderScripts();
+            return;
+        }
+
+        try {
+            const response = await fetch(`/api/launcher-scripts/${groupId}`);
+            this.scripts = await response.json();
+            this.currentGroup = this.groups.find(g => g.id === groupId);
+            this.updateGroupIcon();
+            this.renderScripts();
+        } catch (error) {
+            console.error('Error loading launcher scripts:', error);
+            this.scripts = [];
+            this.renderScripts();
+        }
+    }
+
+    updateGroupIcon() {
+        const iconElement = document.getElementById('selected-group-icon');
+        if (!iconElement || !this.currentGroup) return;
+
+        // Intentar cargar icono personalizado
+        const img = document.createElement('img');
+        img.src = `/api/group-icon/launcher/${this.currentGroup.id}`;
+        img.className = 'w-6 h-6 rounded';
+        img.onerror = () => {
+            // Fallback a icono por defecto
+            iconElement.innerHTML = this.getDefaultIconForCategory(this.currentGroup.category);
+        };
+        img.onload = () => {
+            iconElement.innerHTML = '';
+            iconElement.appendChild(img);
+        };
+    }
+
+    getDefaultIconForCategory(category) {
+        const icons = {
+            'Herramientas': '🔧',
+            'Análisis': '📊',
+            'Utilidades': '⚙️',
+            'Desarrollo': '💻',
+            'Visualización': '📈',
+            'Otros': '📁'
+        };
+        return icons[category] || '📁';
+    }
+
+    renderScripts() {
+        const grid = document.getElementById('launcher-scripts-grid');
+        if (!grid) return;
+
+        if (this.scripts.length === 0) {
+            grid.innerHTML = `
+                
+ + + +

No hay scripts disponibles

+

Selecciona un grupo o verifica que el directorio contenga archivos .py

+
+ `; + return; + } + + let filteredScripts = this.scripts; + if (this.currentFilter !== 'all' && this.currentGroup) { + if (this.currentGroup.category !== this.currentFilter) { + filteredScripts = []; + } + } + + grid.innerHTML = ''; + filteredScripts.forEach(script => { + const favoriteId = `${this.currentGroup.id}_${script.name}`; + const isFavorite = this.favorites.has(favoriteId); + + const card = document.createElement('div'); + card.className = `script-card ${isFavorite ? 'favorited' : ''}`; + card.innerHTML = ` +
+

${script.display_name}

+ +
+

Script: ${script.name}

+
+ ${this.currentGroup.category} +
+ + +
+
+ `; + grid.appendChild(card); + }); + } + + filterByCategory(category) { + this.currentFilter = category; + + // Actualizar botones activos + document.querySelectorAll('.category-btn').forEach(btn => { + btn.classList.remove('active'); + }); + document.querySelector(`[data-category="${category}"]`).classList.add('active'); + + // Filtrar grupos en el selector + const selector = document.getElementById('launcher-group-select'); + Array.from(selector.options).forEach(option => { + if (option.value === '') return; + option.style.display = (category === 'all' || option.dataset.category === category) ? '' : 'none'; + }); + + // Re-renderizar scripts si hay grupo seleccionado + if (this.currentGroup) { + this.renderScripts(); + } + } + + async toggleFavorite(groupId, scriptName) { + try { + const response = await fetch('/api/launcher-favorites', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + group_id: groupId, + script_name: scriptName + }) + }); + + const result = await response.json(); + if (result.status === 'success') { + const favoriteId = `${groupId}_${scriptName}`; + if (result.action === 'added') { + this.favorites.add(favoriteId); + } else { + this.favorites.delete(favoriteId); + } + + // Recargar datos y re-renderizar + await this.loadFavorites(); + this.renderScripts(); + this.updateFavoritesCount(); + } + } catch (error) { + console.error('Error toggling favorite:', error); + } + } + + async executeScript(scriptName, args = []) { + if (!this.currentGroup) return; + + try { + const response = await fetch('/api/execute-gui-script', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + group_id: this.currentGroup.id, + script_name: scriptName, + args: args + }) + }); + + const result = await response.json(); + if (result.status === 'success') { + // Recargar historial + await this.loadHistory(); + } + } catch (error) { + console.error('Error executing script:', error); + } + } + + showArgsModal(scriptName, displayName) { + const modal = document.getElementById('script-args-modal'); + const scriptDisplayElement = document.getElementById('script-display-name'); + const argsInput = document.getElementById('script-args-input'); + + if (modal && scriptDisplayElement && argsInput) { + scriptDisplayElement.textContent = displayName; + argsInput.value = ''; + modal.classList.remove('hidden'); + + // Guardar datos para uso posterior + modal.dataset.scriptName = scriptName; + modal.dataset.groupId = this.currentGroup.id; + } + } + + renderFavorites(favorites) { + const favoritesList = document.getElementById('favorites-list'); + const favoritesPanel = document.getElementById('favorites-panel'); + + if (!favoritesList || !favoritesPanel) return; + + if (favorites.length === 0) { + favoritesPanel.classList.add('empty'); + return; + } + + favoritesPanel.classList.remove('empty'); + favoritesList.innerHTML = ''; + + favorites.slice(0, 5).forEach(fav => { + const group = this.groups.find(g => g.id === fav.group_id); + if (!group) return; + + const item = document.createElement('div'); + item.className = 'flex items-center justify-between p-2 bg-white rounded border'; + item.innerHTML = ` +
+
+ ${this.getDefaultIconForCategory(group.category)} +
+
+
${fav.script_name.replace('.py', '')}
+
${group.name}
+
+
+ + `; + favoritesList.appendChild(item); + }); + } + + async executeFavoriteScript(groupId, scriptName) { + // Cambiar al grupo correcto si no está seleccionado + if (!this.currentGroup || this.currentGroup.id !== groupId) { + document.getElementById('launcher-group-select').value = groupId; + await this.loadLauncherScripts(); + } + + this.executeScript(scriptName); + } + + renderHistory() { + const historyList = document.getElementById('history-list'); + if (!historyList) return; + + if (this.history.length === 0) { + historyList.innerHTML = ` +
+

No hay ejecuciones recientes

+
+ `; + return; + } + + historyList.innerHTML = ''; + this.history.slice(0, 10).forEach(entry => { + const group = this.groups.find(g => g.id === entry.group_id); + const groupName = group ? group.name : 'Grupo desconocido'; + + const timeAgo = this.getTimeAgo(entry.executed_date); + const statusClass = entry.status === 'success' ? 'success' : + entry.status === 'error' ? 'error' : 'running'; + const statusIcon = entry.status === 'success' ? '✅' : + entry.status === 'error' ? '❌' : '🔄'; + + const item = document.createElement('div'); + item.className = `history-item ${statusClass}`; + item.innerHTML = ` +
+
+ ${entry.script_name.replace('.py', '')} + ${groupName} +
+ ${timeAgo} +
+
+ ${statusIcon} ${entry.status.charAt(0).toUpperCase() + entry.status.slice(1)} + ${entry.execution_time ? ` - ${entry.execution_time}s` : ''} + ${entry.arguments && entry.arguments.length > 0 ? ` - Con argumentos` : ''} +
+ `; + historyList.appendChild(item); + }); + } + + getTimeAgo(dateString) { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now - date; + const diffMinutes = Math.floor(diffMs / 60000); + + if (diffMinutes < 1) return 'ahora'; + if (diffMinutes < 60) return `hace ${diffMinutes}m`; + if (diffMinutes < 1440) return `hace ${Math.floor(diffMinutes / 60)}h`; + return `hace ${Math.floor(diffMinutes / 1440)}d`; + } + + updateFavoritesCount() { + const countElement = document.getElementById('favorites-count'); + if (countElement) { + countElement.textContent = `${this.favorites.size} favoritos`; + } + } + + async clearLauncherHistory() { + if (!confirm('¿Estás seguro de que quieres limpiar el historial?')) return; + + try { + const response = await fetch('/api/launcher-history', { + method: 'DELETE' + }); + + const result = await response.json(); + if (result.status === 'success') { + this.history = []; + this.renderHistory(); + } + } catch (error) { + console.error('Error clearing history:', error); + } + } + + // === GESTIÓN DE GRUPOS === + + openGroupEditor() { + const modal = document.getElementById('group-editor-modal'); + if (modal) { + this.currentEditingGroup = null; + this.clearGroupForm(); + this.renderExistingGroups(); + modal.classList.remove('hidden'); + } + } + + closeGroupEditor() { + const modal = document.getElementById('group-editor-modal'); + if (modal) { + modal.classList.add('hidden'); + this.currentEditingGroup = null; + } + } + + renderExistingGroups() { + const container = document.getElementById('existing-groups-list'); + if (!container) return; + + container.innerHTML = ''; + this.groups.forEach(group => { + const item = document.createElement('div'); + item.className = 'group-list-item'; + item.innerHTML = ` +
+ ${this.getDefaultIconForCategory(group.category)} +
+
+
${group.name}
+
${group.category}
+
+ + `; + container.appendChild(item); + }); + } + + editGroup(groupId) { + const group = this.groups.find(g => g.id === groupId); + if (!group) return; + + this.currentEditingGroup = group; + this.populateGroupForm(group); + document.getElementById('delete-group-btn').style.display = 'block'; + } + + populateGroupForm(group) { + document.getElementById('group-id').value = group.id; + document.getElementById('group-name').value = group.name; + document.getElementById('group-description').value = group.description || ''; + document.getElementById('group-category').value = group.category; + document.getElementById('group-version').value = group.version || '1.0'; + document.getElementById('group-directory').value = group.directory; + } + + clearGroupForm() { + document.getElementById('group-id').value = ''; + document.getElementById('group-name').value = ''; + document.getElementById('group-description').value = ''; + document.getElementById('group-category').value = 'Otros'; + document.getElementById('group-version').value = '1.0'; + document.getElementById('group-directory').value = ''; + document.getElementById('delete-group-btn').style.display = 'none'; + } + + async saveGroup() { + const formData = { + id: document.getElementById('group-id').value, + name: document.getElementById('group-name').value, + description: document.getElementById('group-description').value, + category: document.getElementById('group-category').value, + version: document.getElementById('group-version').value, + directory: document.getElementById('group-directory').value + }; + + try { + let response; + if (this.currentEditingGroup) { + // Actualizar grupo existente + response = await fetch(`/api/launcher-groups/${this.currentEditingGroup.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }); + } else { + // Crear nuevo grupo + response = await fetch('/api/launcher-groups', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }); + } + + const result = await response.json(); + if (result.status === 'success') { + await this.loadGroups(); + this.closeGroupEditor(); + this.renderInterface(); + } else { + alert(`Error: ${result.message}`); + } + } catch (error) { + console.error('Error saving group:', error); + alert('Error al guardar el grupo'); + } + } + + async deleteGroup() { + if (!this.currentEditingGroup) return; + + if (!confirm(`¿Estás seguro de que quieres eliminar el grupo "${this.currentEditingGroup.name}"?`)) return; + + try { + const response = await fetch(`/api/launcher-groups/${this.currentEditingGroup.id}`, { + method: 'DELETE' + }); + + const result = await response.json(); + if (result.status === 'success') { + await this.loadGroups(); + this.closeGroupEditor(); + this.renderInterface(); + + // Limpiar selección si era el grupo actual + if (this.currentGroup && this.currentGroup.id === this.currentEditingGroup.id) { + document.getElementById('launcher-group-select').value = ''; + this.currentGroup = null; + this.scripts = []; + this.renderScripts(); + } + } else { + alert(`Error: ${result.message}`); + } + } catch (error) { + console.error('Error deleting group:', error); + alert('Error al eliminar el grupo'); + } + } + + browseGroupDirectory() { + // Similar a la función existente pero para el formulario de grupos + fetch('/api/browse-directories') + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + document.getElementById('group-directory').value = data.path; + } + }) + .catch(error => { + console.error('Error browsing directory:', error); + }); + } +} + +// === FUNCIONES GLOBALES === + +// Función para cambiar entre tabs +function switchTab(tabName) { + // Cambiar tabs activos + document.querySelectorAll('.tab-button').forEach(btn => { + btn.classList.remove('active'); + }); + document.getElementById(`${tabName}-tab`).classList.add('active'); + + // Cambiar contenido + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.add('hidden'); + }); + document.getElementById(`${tabName}-content`).classList.remove('hidden'); + + // Inicializar launcher si es la primera vez + if (tabName === 'launcher' && !window.launcherManager) { + window.launcherManager = new LauncherManager(); + window.launcherManager.init(); + } +} + +// Funciones para modales +function openGroupEditor() { + if (window.launcherManager) { + window.launcherManager.openGroupEditor(); + } +} + +function closeGroupEditor() { + if (window.launcherManager) { + window.launcherManager.closeGroupEditor(); + } +} + +function deleteGroup() { + if (window.launcherManager) { + window.launcherManager.deleteGroup(); + } +} + +function browseGroupDirectory() { + if (window.launcherManager) { + window.launcherManager.browseGroupDirectory(); + } +} + +function filterByCategory(category) { + if (window.launcherManager) { + window.launcherManager.filterByCategory(category); + } +} + +function loadLauncherScripts() { + if (window.launcherManager) { + window.launcherManager.loadLauncherScripts(); + } +} + +function clearLauncherHistory() { + if (window.launcherManager) { + window.launcherManager.clearLauncherHistory(); + } +} + +// Funciones para modal de argumentos +function closeArgsModal() { + const modal = document.getElementById('script-args-modal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +function executeWithArgs() { + const modal = document.getElementById('script-args-modal'); + const argsInput = document.getElementById('script-args-input'); + + if (modal && argsInput && window.launcherManager) { + const scriptName = modal.dataset.scriptName; + const args = argsInput.value.trim().split(/\s+/).filter(arg => arg.length > 0); + + window.launcherManager.executeScript(scriptName, args); + closeArgsModal(); + } +} + +// Inicialización cuando se carga la página +document.addEventListener('DOMContentLoaded', function () { + console.log('Launcher JS loaded'); +}); \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 2e046aa..14e84c6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,6 @@ - + + @@ -7,13 +8,17 @@ +
@@ -26,7 +31,8 @@

Configuración Global

@@ -35,8 +41,7 @@

Configuración Base

-
@@ -53,9 +58,8 @@
-

Configuración del Scrips

-
@@ -71,10 +75,12 @@
- -
@@ -83,84 +89,214 @@
- -
-

Funciones a utilizar:

-
- - - -
-

-
- - + +
+
+
- -
-

Directorio de Trabajo

-
-
- - -
- +

+
+ + +
- -
- + + +
+

Directorio de Trabajo

+
+
+ + + +
+ +
+ +
+ +
+
+ + +
+
+

Configuración del Directorio

+ +
+
+
+
+ +
+
+
+ + +
+

Scripts Disponibles

+
- -
-
-

Configuración del Directorio

- -
-
-
-
-
+ + +
+ +
+ +
+
📁
+
+
+
+ + +
+

Filtrar por Categoría

+
+ + + + +
+
+
+ + +
+
+

+ ⭐ Scripts Favoritos +

+ + 0 favoritos + +
+
+ +
+
+ + +
+

Scripts Disponibles

+
+ +
+
+ + +
+
+

📝 Historial Reciente

+ +
+
+ +
- -
-

Scripts Disponibles

-
-
- - +

Logs

@@ -168,12 +304,13 @@ Limpiar
-
+
- + - +
@@ -211,8 +349,7 @@ class="bg-gray-500 text-white px-4 py-2 rounded"> Cancelar -
@@ -220,13 +357,15 @@
- -