Implementación del Launcher C# y mejoras en la interfaz de usuario
- Se añadió un nuevo launcher para proyectos C# que permite gestionar, ejecutar y categorizar aplicaciones compiladas. - Se implementaron tres pestañas en la interfaz: "Scripts (Config)", "Launcher GUI (Python)" y "Launcher C#", mejorando la organización y accesibilidad. - Se actualizaron los archivos de configuración y se mejoró la lógica de inicialización para soportar el nuevo sistema de C#. - Se realizaron ajustes en la interfaz para incluir un panel de favoritos y un sistema de gestión de procesos en ejecución para C#. - Se mejoró la documentación en `adicion_launcher4GUI.md` para reflejar las nuevas funcionalidades y estructura del proyecto.
This commit is contained in:
parent
bf30b2db52
commit
7ab11a94ce
File diff suppressed because it is too large
Load Diff
4
app.py
4
app.py
|
@ -2,6 +2,7 @@ from flask import Flask, render_template, request, jsonify, url_for
|
||||||
from flask_sock import Sock
|
from flask_sock import Sock
|
||||||
from lib.config_manager import ConfigurationManager
|
from lib.config_manager import ConfigurationManager
|
||||||
from lib.launcher_manager import LauncherManager
|
from lib.launcher_manager import LauncherManager
|
||||||
|
from lib.csharp_launcher_manager import CSharpLauncherManager
|
||||||
import os
|
import os
|
||||||
import json # Added import
|
import json # Added import
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -27,6 +28,9 @@ config_manager = ConfigurationManager()
|
||||||
# Inicializar launcher manager
|
# Inicializar launcher manager
|
||||||
launcher_manager = LauncherManager(config_manager.data_path)
|
launcher_manager = LauncherManager(config_manager.data_path)
|
||||||
|
|
||||||
|
# Inicializar C# launcher manager
|
||||||
|
csharp_launcher_manager = CSharpLauncherManager(config_manager.data_path)
|
||||||
|
|
||||||
# Lista global para mantener las conexiones WebSocket activas
|
# Lista global para mantener las conexiones WebSocket activas
|
||||||
websocket_connections = set()
|
websocket_connections = set()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "C:/Program Files/Siemens/Automation/Portal V19/PublicAPI/V19/Schemas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../../../../Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/Source/98050_PLC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../../../../Trabajo/VM/22 - 93841 - Sidel - Tilting/Reporte/TiaExports"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
|
@ -1,5 +1,31 @@
|
||||||
{
|
{
|
||||||
"history": [
|
"history": [
|
||||||
|
{
|
||||||
|
"id": "b321622a",
|
||||||
|
"group_id": "4",
|
||||||
|
"script_name": "x1.py",
|
||||||
|
"executed_date": "2025-06-15T18:19:14.681042Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 27400,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "754d0df9",
|
||||||
|
"group_id": "4",
|
||||||
|
"script_name": "x1.py",
|
||||||
|
"executed_date": "2025-06-15T18:01:45.840069Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 38228,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "15176a5f",
|
"id": "15176a5f",
|
||||||
"group_id": "1",
|
"group_id": "1",
|
||||||
|
|
5873
data/log.txt
5873
data/log.txt
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,740 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
import glob
|
||||||
|
|
||||||
|
class CSharpLauncherManager:
|
||||||
|
def __init__(self, data_path: str):
|
||||||
|
self.data_path = data_path
|
||||||
|
self.launcher_config_path = os.path.join(data_path, "csharp_launcher_projects.json")
|
||||||
|
self.favorites_path = os.path.join(data_path, "csharp_launcher_favorites.json")
|
||||||
|
self.script_metadata_path = os.path.join(data_path, "csharp_launcher_metadata.json")
|
||||||
|
|
||||||
|
# Procesos en ejecución para C#
|
||||||
|
self.running_processes = {}
|
||||||
|
self.process_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Inicializar archivos si no existen
|
||||||
|
self._initialize_files()
|
||||||
|
|
||||||
|
def _initialize_files(self):
|
||||||
|
"""Crear archivos de configuración por defecto si no existen"""
|
||||||
|
# Inicializar csharp_launcher_projects.json
|
||||||
|
if not os.path.exists(self.launcher_config_path):
|
||||||
|
default_config = {
|
||||||
|
"version": "1.0",
|
||||||
|
"projects": [],
|
||||||
|
"categories": {
|
||||||
|
"Aplicaciones": {
|
||||||
|
"color": "#3B82F6",
|
||||||
|
"icon": "🖥️",
|
||||||
|
"subcategories": ["Desktop", "Consola", "WPF"]
|
||||||
|
},
|
||||||
|
"Herramientas": {
|
||||||
|
"color": "#10B981",
|
||||||
|
"icon": "🔧",
|
||||||
|
"subcategories": ["Utilidades", "Automatización", "Sistema"]
|
||||||
|
},
|
||||||
|
"Análisis": {
|
||||||
|
"color": "#8B5CF6",
|
||||||
|
"icon": "📊",
|
||||||
|
"subcategories": ["Datos", "Reportes", "Business Intelligence"]
|
||||||
|
},
|
||||||
|
"Desarrollo": {
|
||||||
|
"color": "#F59E0B",
|
||||||
|
"icon": "💻",
|
||||||
|
"subcategories": ["Testing", "Build Tools", "DevOps"]
|
||||||
|
},
|
||||||
|
"APIs": {
|
||||||
|
"color": "#EF4444",
|
||||||
|
"icon": "🌐",
|
||||||
|
"subcategories": ["REST", "GraphQL", "Microservicios"]
|
||||||
|
},
|
||||||
|
"Otros": {
|
||||||
|
"color": "#6B7280",
|
||||||
|
"icon": "📁",
|
||||||
|
"subcategories": ["Misceláneos"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"default_execution_directory": "project_directory",
|
||||||
|
"search_debug_first": True,
|
||||||
|
"show_build_output": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(default_config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Inicializar csharp_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 csharp_launcher_metadata.json
|
||||||
|
if not os.path.exists(self.script_metadata_path):
|
||||||
|
default_metadata = {
|
||||||
|
"version": "1.0",
|
||||||
|
"executable_metadata": {}
|
||||||
|
}
|
||||||
|
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(default_metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def get_csharp_projects(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Obtener todos los proyectos C#"""
|
||||||
|
try:
|
||||||
|
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
return config.get("projects", [])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading C# projects: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_csharp_project(self, project_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Obtener un proyecto específico por ID"""
|
||||||
|
projects = self.get_csharp_projects()
|
||||||
|
for project in projects:
|
||||||
|
if project.get("id") == project_id:
|
||||||
|
return project
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_csharp_project(self, project_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Agregar nuevo proyecto C#"""
|
||||||
|
try:
|
||||||
|
# Validar datos requeridos
|
||||||
|
required_fields = ["name", "directory"]
|
||||||
|
for field in required_fields:
|
||||||
|
if not project_data.get(field):
|
||||||
|
return {"status": "error", "message": f"Campo requerido: {field}"}
|
||||||
|
|
||||||
|
# Validar que el directorio existe
|
||||||
|
if not os.path.isdir(project_data["directory"]):
|
||||||
|
return {"status": "error", "message": "El directorio especificado no existe"}
|
||||||
|
|
||||||
|
# Generar ID único si no se proporciona
|
||||||
|
if not project_data.get("id"):
|
||||||
|
project_data["id"] = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
# Verificar que el ID no exista
|
||||||
|
if self.get_csharp_project(project_data["id"]):
|
||||||
|
return {"status": "error", "message": "Ya existe un proyecto con este ID"}
|
||||||
|
|
||||||
|
# Agregar campos por defecto
|
||||||
|
current_time = datetime.now().isoformat() + "Z"
|
||||||
|
project_data.setdefault("description", "")
|
||||||
|
project_data.setdefault("category", "Otros")
|
||||||
|
project_data.setdefault("version", "1.0")
|
||||||
|
project_data.setdefault("author", "")
|
||||||
|
project_data.setdefault("tags", [])
|
||||||
|
project_data.setdefault("dotnet_version", "") # Versión de .NET
|
||||||
|
project_data.setdefault("created_date", current_time)
|
||||||
|
project_data["updated_date"] = current_time
|
||||||
|
|
||||||
|
# Cargar configuración y agregar proyecto
|
||||||
|
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
config["projects"].append(project_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": "Proyecto agregado exitosamente"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error agregando proyecto: {str(e)}"}
|
||||||
|
|
||||||
|
def update_csharp_project(self, project_id: str, project_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Actualizar proyecto existente"""
|
||||||
|
try:
|
||||||
|
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Buscar y actualizar el proyecto
|
||||||
|
project_found = False
|
||||||
|
for i, project in enumerate(config["projects"]):
|
||||||
|
if project["id"] == project_id:
|
||||||
|
# Mantener ID y fechas de creación
|
||||||
|
project_data["id"] = project_id
|
||||||
|
project_data["created_date"] = project.get("created_date", datetime.now().isoformat() + "Z")
|
||||||
|
project_data["updated_date"] = datetime.now().isoformat() + "Z"
|
||||||
|
|
||||||
|
config["projects"][i] = project_data
|
||||||
|
project_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not project_found:
|
||||||
|
return {"status": "error", "message": "Proyecto 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": "Proyecto actualizado exitosamente"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error actualizando proyecto: {str(e)}"}
|
||||||
|
|
||||||
|
def delete_csharp_project(self, project_id: str) -> Dict[str, str]:
|
||||||
|
"""Eliminar proyecto C#"""
|
||||||
|
try:
|
||||||
|
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Filtrar el proyecto a eliminar
|
||||||
|
original_count = len(config["projects"])
|
||||||
|
config["projects"] = [p for p in config["projects"] if p["id"] != project_id]
|
||||||
|
|
||||||
|
if len(config["projects"]) == original_count:
|
||||||
|
return {"status": "error", "message": "Proyecto no encontrado"}
|
||||||
|
|
||||||
|
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Limpiar metadatos y favoritos relacionados
|
||||||
|
self._cleanup_metadata_for_project(project_id)
|
||||||
|
self._cleanup_favorites_for_project(project_id)
|
||||||
|
|
||||||
|
return {"status": "success", "message": "Proyecto eliminado exitosamente"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"}
|
||||||
|
|
||||||
|
def get_project_executables(self, project_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Obtener ejecutables de un proyecto específico (solo visibles)"""
|
||||||
|
project = self.get_csharp_project(project_id)
|
||||||
|
if not project:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_dir = project["directory"]
|
||||||
|
if not os.path.isdir(project_dir):
|
||||||
|
return []
|
||||||
|
|
||||||
|
executables = []
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
|
||||||
|
# Buscar en bin/Release y bin/Debug
|
||||||
|
search_patterns = [
|
||||||
|
os.path.join(project_dir, "**/bin/Release/**/*.exe"),
|
||||||
|
os.path.join(project_dir, "**/bin/Debug/**/*.exe")
|
||||||
|
]
|
||||||
|
|
||||||
|
found_exe_files = set()
|
||||||
|
for pattern in search_patterns:
|
||||||
|
for exe_path in glob.glob(pattern, recursive=True):
|
||||||
|
if os.path.isfile(exe_path):
|
||||||
|
found_exe_files.add(exe_path)
|
||||||
|
|
||||||
|
for exe_path in found_exe_files:
|
||||||
|
exe_name = os.path.basename(exe_path)
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
|
||||||
|
# Obtener metadatos o crear por defecto
|
||||||
|
exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {})
|
||||||
|
|
||||||
|
# Solo incluir si no está oculto
|
||||||
|
if not exe_metadata.get("hidden", False):
|
||||||
|
# Determinar si es Debug o Release
|
||||||
|
build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug"
|
||||||
|
|
||||||
|
executables.append({
|
||||||
|
"filename": exe_name,
|
||||||
|
"full_path": exe_path,
|
||||||
|
"display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')),
|
||||||
|
"short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"),
|
||||||
|
"long_description": exe_metadata.get("long_description", ""),
|
||||||
|
"build_type": build_type,
|
||||||
|
"relative_path": os.path.relpath(exe_path, project_dir)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ordenar por nombre de display
|
||||||
|
executables.sort(key=lambda x: x['display_name'])
|
||||||
|
return executables
|
||||||
|
|
||||||
|
def get_all_project_executables(self, project_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Obtener TODOS los ejecutables de un proyecto (incluyendo ocultos) para gestión"""
|
||||||
|
project = self.get_csharp_project(project_id)
|
||||||
|
if not project:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_dir = project["directory"]
|
||||||
|
if not os.path.isdir(project_dir):
|
||||||
|
return []
|
||||||
|
|
||||||
|
executables = []
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
|
||||||
|
# Buscar en bin/Release y bin/Debug
|
||||||
|
search_patterns = [
|
||||||
|
os.path.join(project_dir, "**/bin/Release/**/*.exe"),
|
||||||
|
os.path.join(project_dir, "**/bin/Debug/**/*.exe")
|
||||||
|
]
|
||||||
|
|
||||||
|
found_exe_files = set()
|
||||||
|
for pattern in search_patterns:
|
||||||
|
for exe_path in glob.glob(pattern, recursive=True):
|
||||||
|
if os.path.isfile(exe_path):
|
||||||
|
found_exe_files.add(exe_path)
|
||||||
|
|
||||||
|
for exe_path in found_exe_files:
|
||||||
|
exe_name = os.path.basename(exe_path)
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
|
||||||
|
# Obtener metadatos o crear por defecto
|
||||||
|
exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {})
|
||||||
|
|
||||||
|
# Determinar si es Debug o Release
|
||||||
|
build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug"
|
||||||
|
|
||||||
|
executables.append({
|
||||||
|
"filename": exe_name,
|
||||||
|
"full_path": exe_path,
|
||||||
|
"display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')),
|
||||||
|
"short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"),
|
||||||
|
"long_description": exe_metadata.get("long_description", ""),
|
||||||
|
"build_type": build_type,
|
||||||
|
"relative_path": os.path.relpath(exe_path, project_dir),
|
||||||
|
"hidden": exe_metadata.get("hidden", False)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ordenar por nombre de display
|
||||||
|
executables.sort(key=lambda x: x['display_name'])
|
||||||
|
return executables
|
||||||
|
|
||||||
|
def get_executable_metadata(self, project_id: str, exe_name: str) -> Dict[str, Any]:
|
||||||
|
"""Obtener metadatos de un ejecutable específico"""
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
return metadata.get("executable_metadata", {}).get(exe_key, {
|
||||||
|
"display_name": exe_name.replace('.exe', ''),
|
||||||
|
"short_description": "Aplicación C#",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": False
|
||||||
|
})
|
||||||
|
|
||||||
|
def update_executable_metadata(self, project_id: str, exe_name: str, metadata_update: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Actualizar metadatos de un ejecutable específico"""
|
||||||
|
try:
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
|
||||||
|
if "executable_metadata" not in metadata:
|
||||||
|
metadata["executable_metadata"] = {}
|
||||||
|
|
||||||
|
if exe_key not in metadata["executable_metadata"]:
|
||||||
|
metadata["executable_metadata"][exe_key] = {}
|
||||||
|
|
||||||
|
# Actualizar campos
|
||||||
|
metadata["executable_metadata"][exe_key].update(metadata_update)
|
||||||
|
|
||||||
|
self._save_executable_metadata(metadata)
|
||||||
|
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
|
||||||
|
|
||||||
|
def execute_csharp_executable(self, project_id: str, exe_name: str, exe_args: List[str],
|
||||||
|
broadcast_func, working_dir: str = None) -> Dict[str, Any]:
|
||||||
|
"""Ejecutar un ejecutable C#"""
|
||||||
|
try:
|
||||||
|
project = self.get_csharp_project(project_id)
|
||||||
|
if not project:
|
||||||
|
return {"status": "error", "message": "Proyecto no encontrado"}
|
||||||
|
|
||||||
|
# Buscar el ejecutable
|
||||||
|
executables = self.get_all_project_executables(project_id)
|
||||||
|
exe_info = None
|
||||||
|
for exe in executables:
|
||||||
|
if exe["filename"] == exe_name:
|
||||||
|
exe_info = exe
|
||||||
|
break
|
||||||
|
|
||||||
|
if not exe_info:
|
||||||
|
return {"status": "error", "message": f"Ejecutable '{exe_name}' no encontrado"}
|
||||||
|
|
||||||
|
exe_path = exe_info["full_path"]
|
||||||
|
|
||||||
|
# Determinar directorio de trabajo
|
||||||
|
if working_dir and os.path.isdir(working_dir):
|
||||||
|
work_dir = working_dir
|
||||||
|
else:
|
||||||
|
work_dir = os.path.dirname(exe_path)
|
||||||
|
|
||||||
|
# Preparar comando
|
||||||
|
cmd = [exe_path] + exe_args
|
||||||
|
|
||||||
|
execution_id = str(uuid.uuid4())[:8]
|
||||||
|
|
||||||
|
broadcast_func(f"🚀 Ejecutando: {exe_info['display_name']}")
|
||||||
|
broadcast_func(f"📁 Directorio: {work_dir}")
|
||||||
|
broadcast_func(f"⚡ Comando: {' '.join(cmd)}")
|
||||||
|
broadcast_func("=" * 50)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ejecutar el proceso
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=work_dir,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
universal_newlines=True,
|
||||||
|
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Almacenar información del proceso
|
||||||
|
with self.process_lock:
|
||||||
|
self.running_processes[process.pid] = {
|
||||||
|
"project_id": project_id,
|
||||||
|
"exe_name": exe_name,
|
||||||
|
"display_name": exe_info['display_name'],
|
||||||
|
"start_time": datetime.now(),
|
||||||
|
"process": process
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}")
|
||||||
|
|
||||||
|
# Monitorear salida en hilo separado
|
||||||
|
def read_output():
|
||||||
|
try:
|
||||||
|
for line in iter(process.stdout.readline, ''):
|
||||||
|
if line.strip():
|
||||||
|
broadcast_func(line.rstrip())
|
||||||
|
except Exception as e:
|
||||||
|
broadcast_func(f"Error leyendo salida: {e}")
|
||||||
|
finally:
|
||||||
|
if process.stdout:
|
||||||
|
process.stdout.close()
|
||||||
|
|
||||||
|
output_thread = threading.Thread(target=read_output, daemon=True)
|
||||||
|
output_thread.start()
|
||||||
|
|
||||||
|
# Monitorear finalización en hilo separado
|
||||||
|
def monitor_completion():
|
||||||
|
try:
|
||||||
|
start_time = time.time()
|
||||||
|
return_code = process.wait()
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Limpiar de procesos en ejecución
|
||||||
|
with self.process_lock:
|
||||||
|
if process.pid in self.running_processes:
|
||||||
|
del self.running_processes[process.pid]
|
||||||
|
|
||||||
|
if return_code == 0:
|
||||||
|
broadcast_func(f"✅ Proceso completado exitosamente (PID: {process.pid})")
|
||||||
|
else:
|
||||||
|
broadcast_func(f"❌ Proceso terminó con código: {return_code} (PID: {process.pid})")
|
||||||
|
|
||||||
|
broadcast_func(f"⏱️ Tiempo de ejecución: {execution_time:.2f} segundos")
|
||||||
|
broadcast_func("=" * 50)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
broadcast_func(f"Error monitoreando proceso: {e}")
|
||||||
|
|
||||||
|
monitor_thread = threading.Thread(target=monitor_completion, daemon=True)
|
||||||
|
monitor_thread.start()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Ejecutable '{exe_info['display_name']}' iniciado",
|
||||||
|
"pid": process.pid,
|
||||||
|
"execution_id": execution_id
|
||||||
|
}
|
||||||
|
|
||||||
|
except subprocess.SubprocessError as e:
|
||||||
|
return {"status": "error", "message": f"Error ejecutando el proceso: {str(e)}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error inesperado: {str(e)}"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error ejecutando ejecutable: {str(e)}"}
|
||||||
|
|
||||||
|
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 C# favorites: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def toggle_favorite(self, project_id: str, exe_name: str) -> Dict[str, str]:
|
||||||
|
"""Agregar o quitar de favoritos"""
|
||||||
|
try:
|
||||||
|
with open(self.favorites_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
favorites = data.get("favorites", [])
|
||||||
|
favorite_key = f"{project_id}_{exe_name}"
|
||||||
|
|
||||||
|
# Buscar si ya existe
|
||||||
|
existing_favorite = None
|
||||||
|
for fav in favorites:
|
||||||
|
if fav.get("project_id") == project_id and fav.get("exe_name") == exe_name:
|
||||||
|
existing_favorite = fav
|
||||||
|
break
|
||||||
|
|
||||||
|
if existing_favorite:
|
||||||
|
# Quitar de favoritos
|
||||||
|
favorites.remove(existing_favorite)
|
||||||
|
message = "Removido de favoritos"
|
||||||
|
is_favorite = False
|
||||||
|
else:
|
||||||
|
# Agregar a favoritos
|
||||||
|
favorites.append({
|
||||||
|
"id": favorite_key,
|
||||||
|
"project_id": project_id,
|
||||||
|
"exe_name": exe_name,
|
||||||
|
"added_date": datetime.now().isoformat() + "Z"
|
||||||
|
})
|
||||||
|
message = "Agregado a favoritos"
|
||||||
|
is_favorite = True
|
||||||
|
|
||||||
|
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",
|
||||||
|
"message": message,
|
||||||
|
"is_favorite": is_favorite
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error gestionando favorito: {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 C# categories: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_running_processes(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Obtener procesos C# en ejecución"""
|
||||||
|
with self.process_lock:
|
||||||
|
processes = []
|
||||||
|
for pid, info in self.running_processes.items():
|
||||||
|
try:
|
||||||
|
# Verificar si el proceso sigue activo
|
||||||
|
if info["process"].poll() is None:
|
||||||
|
processes.append({
|
||||||
|
"pid": pid,
|
||||||
|
"project_id": info["project_id"],
|
||||||
|
"exe_name": info["exe_name"],
|
||||||
|
"display_name": info["display_name"],
|
||||||
|
"start_time": info["start_time"].isoformat()
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Proceso terminado, remover de la lista
|
||||||
|
del self.running_processes[pid]
|
||||||
|
except:
|
||||||
|
# Error verificando proceso, remover
|
||||||
|
del self.running_processes[pid]
|
||||||
|
|
||||||
|
return processes
|
||||||
|
|
||||||
|
def terminate_process(self, pid: int) -> Dict[str, str]:
|
||||||
|
"""Cerrar un proceso C#"""
|
||||||
|
try:
|
||||||
|
with self.process_lock:
|
||||||
|
if pid in self.running_processes:
|
||||||
|
process_info = self.running_processes[pid]
|
||||||
|
process = process_info["process"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
process.wait(timeout=5)
|
||||||
|
del self.running_processes[pid]
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Proceso {process_info['display_name']} (PID: {pid}) terminado"
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
del self.running_processes[pid]
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Proceso {process_info['display_name']} (PID: {pid}) forzado a cerrar"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {"status": "error", "message": "Proceso no encontrado en ejecución"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error terminando proceso: {str(e)}"}
|
||||||
|
|
||||||
|
def _load_executable_metadata(self) -> Dict[str, Any]:
|
||||||
|
"""Cargar metadatos de ejecutables desde archivo"""
|
||||||
|
try:
|
||||||
|
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {"version": "1.0", "executable_metadata": {}}
|
||||||
|
|
||||||
|
def _save_executable_metadata(self, metadata: Dict[str, Any]):
|
||||||
|
"""Guardar metadatos de ejecutables a archivo"""
|
||||||
|
try:
|
||||||
|
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving C# executable metadata: {e}")
|
||||||
|
|
||||||
|
def _cleanup_metadata_for_project(self, project_id: str):
|
||||||
|
"""Limpiar metadatos de un proyecto eliminado"""
|
||||||
|
try:
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
if "executable_metadata" in metadata:
|
||||||
|
# Remover metadatos que empiecen con project_id_
|
||||||
|
keys_to_remove = [key for key in metadata["executable_metadata"].keys()
|
||||||
|
if key.startswith(f"{project_id}_")]
|
||||||
|
for key in keys_to_remove:
|
||||||
|
del metadata["executable_metadata"][key]
|
||||||
|
self._save_executable_metadata(metadata)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning metadata for project {project_id}: {e}")
|
||||||
|
|
||||||
|
def _cleanup_favorites_for_project(self, project_id: str):
|
||||||
|
"""Limpiar favoritos de un proyecto eliminado"""
|
||||||
|
try:
|
||||||
|
with open(self.favorites_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Filtrar favoritos que no sean del proyecto eliminado
|
||||||
|
data["favorites"] = [fav for fav in data.get("favorites", [])
|
||||||
|
if fav.get("project_id") != project_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 favorites for project {project_id}: {e}")
|
||||||
|
|
||||||
|
def get_all_project_executables(self, project_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Obtener TODOS los ejecutables de un proyecto (incluyendo ocultos) para gestión"""
|
||||||
|
project = self.get_csharp_project(project_id)
|
||||||
|
if not project:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_dir = project["directory"]
|
||||||
|
if not os.path.isdir(project_dir):
|
||||||
|
return []
|
||||||
|
|
||||||
|
executables = []
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
|
||||||
|
# Buscar en bin/Release y bin/Debug
|
||||||
|
search_patterns = [
|
||||||
|
os.path.join(project_dir, "**/bin/Release/**/*.exe"),
|
||||||
|
os.path.join(project_dir, "**/bin/Debug/**/*.exe")
|
||||||
|
]
|
||||||
|
|
||||||
|
found_exe_files = set()
|
||||||
|
for pattern in search_patterns:
|
||||||
|
for exe_path in glob.glob(pattern, recursive=True):
|
||||||
|
if os.path.isfile(exe_path):
|
||||||
|
found_exe_files.add(exe_path)
|
||||||
|
|
||||||
|
for exe_path in found_exe_files:
|
||||||
|
exe_name = os.path.basename(exe_path)
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
|
||||||
|
# Obtener metadatos o crear por defecto
|
||||||
|
exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {})
|
||||||
|
|
||||||
|
# Determinar si es Debug o Release
|
||||||
|
build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug"
|
||||||
|
|
||||||
|
executables.append({
|
||||||
|
"filename": exe_name,
|
||||||
|
"full_path": exe_path,
|
||||||
|
"display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')),
|
||||||
|
"short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"),
|
||||||
|
"long_description": exe_metadata.get("long_description", ""),
|
||||||
|
"build_type": build_type,
|
||||||
|
"relative_path": os.path.relpath(exe_path, project_dir),
|
||||||
|
"hidden": exe_metadata.get("hidden", False)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ordenar por nombre de display
|
||||||
|
executables.sort(key=lambda x: x['display_name'])
|
||||||
|
return executables
|
||||||
|
|
||||||
|
def get_executable_metadata(self, project_id: str, exe_name: str) -> Dict[str, Any]:
|
||||||
|
"""Obtener metadatos de un ejecutable específico"""
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
return metadata.get("executable_metadata", {}).get(exe_key, {
|
||||||
|
"display_name": exe_name.replace('.exe', ''),
|
||||||
|
"short_description": "Aplicación C#",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": False
|
||||||
|
})
|
||||||
|
|
||||||
|
def update_executable_metadata(self, project_id: str, exe_name: str, metadata_update: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Actualizar metadatos de un ejecutable específico"""
|
||||||
|
try:
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
exe_key = f"{project_id}_{exe_name}"
|
||||||
|
|
||||||
|
if "executable_metadata" not in metadata:
|
||||||
|
metadata["executable_metadata"] = {}
|
||||||
|
|
||||||
|
if exe_key not in metadata["executable_metadata"]:
|
||||||
|
metadata["executable_metadata"][exe_key] = {}
|
||||||
|
|
||||||
|
# Actualizar campos
|
||||||
|
metadata["executable_metadata"][exe_key].update(metadata_update)
|
||||||
|
|
||||||
|
self._save_executable_metadata(metadata)
|
||||||
|
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
|
||||||
|
|
||||||
|
def _save_executable_metadata(self, metadata: Dict[str, Any]):
|
||||||
|
"""Guardar metadatos de ejecutables a archivo"""
|
||||||
|
try:
|
||||||
|
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving C# executable metadata: {e}")
|
||||||
|
|
||||||
|
def _cleanup_metadata_for_project(self, project_id: str):
|
||||||
|
"""Limpiar metadatos de un proyecto eliminado"""
|
||||||
|
try:
|
||||||
|
metadata = self._load_executable_metadata()
|
||||||
|
if "executable_metadata" in metadata:
|
||||||
|
# Remover metadatos que empiecen con project_id_
|
||||||
|
keys_to_remove = [key for key in metadata["executable_metadata"].keys()
|
||||||
|
if key.startswith(f"{project_id}_")]
|
||||||
|
for key in keys_to_remove:
|
||||||
|
del metadata["executable_metadata"][key]
|
||||||
|
self._save_executable_metadata(metadata)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning metadata for project {project_id}: {e}")
|
||||||
|
|
||||||
|
def _cleanup_favorites_for_project(self, project_id: str):
|
||||||
|
"""Limpiar favoritos de un proyecto eliminado"""
|
||||||
|
try:
|
||||||
|
with open(self.favorites_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Filtrar favoritos que no sean del proyecto eliminado
|
||||||
|
data["favorites"] = [fav for fav in data.get("favorites", [])
|
||||||
|
if fav.get("project_id") != project_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 favorites for project {project_id}: {e}")
|
|
@ -0,0 +1,467 @@
|
||||||
|
// static/js/csharp_launcher.js - Gestor para Launcher C#
|
||||||
|
|
||||||
|
class CSharpLauncherManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentProject = null;
|
||||||
|
this.projects = [];
|
||||||
|
this.executables = [];
|
||||||
|
this.favorites = new Set();
|
||||||
|
this.runningProcesses = [];
|
||||||
|
this.categories = [
|
||||||
|
'Aplicaciones', 'Herramientas', 'Análisis',
|
||||||
|
'Desarrollo', 'APIs', 'Otros'
|
||||||
|
];
|
||||||
|
this.currentCategory = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
console.log('Initializing C# Launcher Manager...');
|
||||||
|
await this.loadProjects();
|
||||||
|
await this.loadFavorites();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.renderInterface();
|
||||||
|
await this.refreshProcesses();
|
||||||
|
|
||||||
|
// Actualizar procesos cada 10 segundos
|
||||||
|
setInterval(() => this.refreshProcesses(), 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProjects() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/csharp-projects');
|
||||||
|
if (response.ok) {
|
||||||
|
this.projects = await response.json();
|
||||||
|
this.renderProjectSelector();
|
||||||
|
} else {
|
||||||
|
console.error('Error loading C# projects:', await response.text());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading C# projects:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFavorites() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/csharp-favorites');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
this.favorites = new Set(data.favorites.map(fav => `${fav.project_id}_${fav.exe_name}`));
|
||||||
|
this.renderFavoritesPanel();
|
||||||
|
} else {
|
||||||
|
console.error('Error loading C# favorites:', await response.text());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading C# favorites:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Event listeners específicos del launcher C#
|
||||||
|
const projectSelect = document.getElementById('csharp-project-select');
|
||||||
|
if (projectSelect) {
|
||||||
|
projectSelect.addEventListener('change', (e) => this.onProjectChange(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInterface() {
|
||||||
|
this.renderProjectSelector();
|
||||||
|
this.renderCategoryFilter();
|
||||||
|
this.renderFavoritesPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProjectSelector() {
|
||||||
|
const select = document.getElementById('csharp-project-select');
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
select.innerHTML = '<option value="">-- Seleccionar Proyecto --</option>';
|
||||||
|
|
||||||
|
this.projects.forEach(project => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = project.id;
|
||||||
|
option.textContent = project.name;
|
||||||
|
option.setAttribute('data-description', project.description || '');
|
||||||
|
option.setAttribute('data-category', project.category || 'Otros');
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onProjectChange(e) {
|
||||||
|
const projectId = e.target.value;
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
this.currentProject = null;
|
||||||
|
this.hideCSharpProjectButtons();
|
||||||
|
this.clearExecutables();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.currentProject = this.projects.find(p => p.id === projectId);
|
||||||
|
this.showCSharpProjectButtons();
|
||||||
|
await this.loadProjectExecutables(projectId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error changing project:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showCSharpProjectButtons() {
|
||||||
|
const buttons = ['cursor-csharp-btn', 'vs2022-csharp-btn', 'folder-csharp-btn', 'copy-path-csharp-btn'];
|
||||||
|
buttons.forEach(id => {
|
||||||
|
const btn = document.getElementById(id);
|
||||||
|
if (btn) btn.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideCSharpProjectButtons() {
|
||||||
|
const buttons = ['cursor-csharp-btn', 'vs2022-csharp-btn', 'folder-csharp-btn', 'copy-path-csharp-btn'];
|
||||||
|
buttons.forEach(id => {
|
||||||
|
const btn = document.getElementById(id);
|
||||||
|
if (btn) btn.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProjectExecutables(projectId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/csharp-executables/${projectId}`);
|
||||||
|
if (response.ok) {
|
||||||
|
this.executables = await response.json();
|
||||||
|
this.renderExecutables();
|
||||||
|
} else {
|
||||||
|
console.error('Error loading executables:', await response.text());
|
||||||
|
this.executables = [];
|
||||||
|
this.renderExecutables();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading executables:', error);
|
||||||
|
this.executables = [];
|
||||||
|
this.renderExecutables();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderExecutables() {
|
||||||
|
const grid = document.getElementById('csharp-executables-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
// Filtrar por categoría si no es 'all'
|
||||||
|
let filteredExecutables = this.executables;
|
||||||
|
if (this.currentCategory !== 'all' && this.currentProject) {
|
||||||
|
filteredExecutables = this.executables.filter(exe =>
|
||||||
|
this.currentProject.category === this.currentCategory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredExecutables.length === 0) {
|
||||||
|
grid.innerHTML = `
|
||||||
|
<div class="col-span-full text-center py-8 text-gray-500">
|
||||||
|
<div class="text-4xl mb-2">🔍</div>
|
||||||
|
<div>No se encontraron ejecutables en este proyecto</div>
|
||||||
|
<div class="text-sm mt-1">Busque en: bin/Release y bin/Debug</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = filteredExecutables.map(exe => this.createExecutableCard(exe)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
createExecutableCard(exe) {
|
||||||
|
const favoriteKey = `${this.currentProject.id}_${exe.filename}`;
|
||||||
|
const isFavorite = this.favorites.has(favoriteKey);
|
||||||
|
const buildTypeBadge = exe.build_type === 'Release' ?
|
||||||
|
'<span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">Release</span>' :
|
||||||
|
'<span class="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded">Debug</span>';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="executable-card bg-white border rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<h4 class="font-medium text-sm">${exe.display_name}</h4>
|
||||||
|
<button class="favorite-star text-gray-300 hover:text-yellow-500 ${isFavorite ? 'text-yellow-500' : ''}"
|
||||||
|
onclick="csharpLauncherManager.toggleFavorite('${this.currentProject.id}', '${exe.filename}')">⭐</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 mb-2">${exe.short_description}</p>
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
${buildTypeBadge}
|
||||||
|
<span class="text-xs text-gray-500">${exe.filename}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="space-x-1">
|
||||||
|
<button class="text-blue-500 hover:underline text-xs"
|
||||||
|
onclick="showCSharpExecutableArgs('${this.currentProject.id}', '${exe.filename}', '${exe.display_name}')">
|
||||||
|
Con Argumentos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600"
|
||||||
|
onclick="csharpLauncherManager.executeExecutable('${this.currentProject.id}', '${exe.filename}')">
|
||||||
|
Ejecutar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeExecutable(projectId, exeName, args = [], workingDir = null) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/execute-csharp-executable', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_id: projectId,
|
||||||
|
exe_name: exeName,
|
||||||
|
args: args,
|
||||||
|
working_dir: workingDir
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
// Actualizar procesos después de un breve delay
|
||||||
|
setTimeout(() => this.refreshProcesses(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing executable:', error);
|
||||||
|
return { status: 'error', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleFavorite(projectId, exeName) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/csharp-favorites', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_id: projectId,
|
||||||
|
exe_name: exeName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
const favoriteKey = `${projectId}_${exeName}`;
|
||||||
|
|
||||||
|
if (result.is_favorite) {
|
||||||
|
this.favorites.add(favoriteKey);
|
||||||
|
} else {
|
||||||
|
this.favorites.delete(favoriteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderExecutables();
|
||||||
|
this.renderFavoritesPanel();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling favorite:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFavoritesPanel() {
|
||||||
|
const panel = document.getElementById('csharp-favorites-list');
|
||||||
|
const counter = document.getElementById('csharp-favorites-count');
|
||||||
|
|
||||||
|
if (!panel || !counter) return;
|
||||||
|
|
||||||
|
counter.textContent = `${this.favorites.size} favoritos`;
|
||||||
|
|
||||||
|
if (this.favorites.size === 0) {
|
||||||
|
panel.innerHTML = '<div class="text-center text-gray-500 py-2">No hay favoritos guardados</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear lista de favoritos (simplificada - solo mostrar nombres)
|
||||||
|
const favoriteItems = Array.from(this.favorites).map(key => {
|
||||||
|
const [projectId, exeName] = key.split('_', 2);
|
||||||
|
const project = this.projects.find(p => p.id === projectId);
|
||||||
|
const projectName = project ? project.name : 'Proyecto desconocido';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex justify-between items-center bg-white p-2 rounded border">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium text-sm">${exeName}</span>
|
||||||
|
<span class="text-xs text-gray-500 ml-2">${projectName}</span>
|
||||||
|
</div>
|
||||||
|
<button class="text-blue-500 hover:text-blue-700 text-xs"
|
||||||
|
onclick="csharpLauncherManager.executeFavorite('${projectId}', '${exeName}')">
|
||||||
|
Ejecutar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
panel.innerHTML = favoriteItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeFavorite(projectId, exeName) {
|
||||||
|
// Cambiar al proyecto si no está seleccionado
|
||||||
|
if (!this.currentProject || this.currentProject.id !== projectId) {
|
||||||
|
const select = document.getElementById('csharp-project-select');
|
||||||
|
if (select) {
|
||||||
|
select.value = projectId;
|
||||||
|
await this.onProjectChange({ target: { value: projectId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar el ejecutable
|
||||||
|
await this.executeExecutable(projectId, exeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCategoryFilter() {
|
||||||
|
// Las categorías ya están en el HTML, solo necesitamos la funcionalidad
|
||||||
|
console.log('Category filter rendered');
|
||||||
|
}
|
||||||
|
|
||||||
|
filterByCategory(category) {
|
||||||
|
this.currentCategory = category;
|
||||||
|
|
||||||
|
// Actualizar botones activos
|
||||||
|
document.querySelectorAll('.csharp-category-btn').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
if (btn.getAttribute('data-category') === category) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderExecutables();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshProcesses() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/csharp-running-processes');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
this.runningProcesses = data.processes || [];
|
||||||
|
this.renderRunningProcesses();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing processes:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRunningProcesses() {
|
||||||
|
const container = document.getElementById('csharp-running-processes');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (this.runningProcesses.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-center text-gray-500 py-2">No hay procesos C# en ejecución</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processItems = this.runningProcesses.map(process => {
|
||||||
|
const startTime = new Date(process.start_time).toLocaleTimeString();
|
||||||
|
return `
|
||||||
|
<div class="flex justify-between items-center bg-gray-50 p-3 rounded border">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">${process.display_name}</span>
|
||||||
|
<span class="text-sm text-gray-500 block">PID: ${process.pid} | Iniciado: ${startTime}</span>
|
||||||
|
</div>
|
||||||
|
<button class="text-red-500 hover:text-red-700 text-sm"
|
||||||
|
onclick="csharpLauncherManager.terminateProcess(${process.pid})">
|
||||||
|
Cerrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = processItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminateProcess(pid) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/csharp-process-terminate/${pid}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await this.refreshProcesses();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error terminating process:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExecutables() {
|
||||||
|
const grid = document.getElementById('csharp-executables-grid');
|
||||||
|
if (grid) {
|
||||||
|
grid.innerHTML = '<div class="col-span-full text-center py-8 text-gray-500">Selecciona un proyecto para ver los ejecutables</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funciones globales para el HTML
|
||||||
|
function loadCSharpExecutables() {
|
||||||
|
const select = document.getElementById('csharp-project-select');
|
||||||
|
if (select && window.csharpLauncherManager) {
|
||||||
|
window.csharpLauncherManager.onProjectChange({ target: select });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterCSharpByCategory(category) {
|
||||||
|
if (window.csharpLauncherManager) {
|
||||||
|
window.csharpLauncherManager.filterByCategory(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCSharpProcesses() {
|
||||||
|
if (window.csharpLauncherManager) {
|
||||||
|
window.csharpLauncherManager.refreshProcesses();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCSharpProjectEditor() {
|
||||||
|
// TODO: Implementar editor de proyectos C#
|
||||||
|
alert('Editor de proyectos C# - Por implementar');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCSharpProjectInEditor(editor) {
|
||||||
|
if (!window.csharpLauncherManager?.currentProject) {
|
||||||
|
alert('Selecciona un proyecto primero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectId = window.csharpLauncherManager.currentProject.id;
|
||||||
|
|
||||||
|
if (editor === 'cursor') {
|
||||||
|
// Implementar apertura en Cursor
|
||||||
|
alert(`Abriendo proyecto ${projectId} en Cursor - Por implementar`);
|
||||||
|
} else if (editor === 'vs2022') {
|
||||||
|
// Implementar apertura en Visual Studio 2022
|
||||||
|
alert(`Abriendo proyecto ${projectId} en Visual Studio 2022 - Por implementar`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCSharpProjectFolder() {
|
||||||
|
if (!window.csharpLauncherManager?.currentProject) {
|
||||||
|
alert('Selecciona un proyecto primero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementar apertura de carpeta
|
||||||
|
alert('Abriendo carpeta del proyecto - Por implementar');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCSharpProjectPath() {
|
||||||
|
if (!window.csharpLauncherManager?.currentProject) {
|
||||||
|
alert('Selecciona un proyecto primero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementar copia de path
|
||||||
|
navigator.clipboard.writeText(window.csharpLauncherManager.currentProject.directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCSharpExecutableManager() {
|
||||||
|
// TODO: Implementar gestor de ejecutables
|
||||||
|
alert('Gestor de ejecutables C# - Por implementar');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCSharpExecutableArgs(projectId, exeName, displayName) {
|
||||||
|
// TODO: Implementar modal de argumentos para C#
|
||||||
|
const args = prompt(`Argumentos para ${displayName}:`, '');
|
||||||
|
if (args !== null) {
|
||||||
|
const argArray = args.trim() ? args.split(' ') : [];
|
||||||
|
window.csharpLauncherManager.executeExecutable(projectId, exeName, argArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicialización global
|
||||||
|
window.csharpLauncherManager = new CSharpLauncherManager();
|
|
@ -1369,6 +1369,11 @@ function switchTab(tabName) {
|
||||||
window.launcherManager = new LauncherManager();
|
window.launcherManager = new LauncherManager();
|
||||||
window.launcherManager.init();
|
window.launcherManager.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inicializar C# launcher si es la primera vez
|
||||||
|
if (tabName === 'csharp' && !window.csharpLauncherManager.currentProject) {
|
||||||
|
window.csharpLauncherManager.init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funciones para modales
|
// Funciones para modales
|
||||||
|
|
|
@ -114,7 +114,18 @@
|
||||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
|
||||||
</path>
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
Launcher GUI
|
Launcher GUI (Python)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button id="csharp-tab" onclick="switchTab('csharp')"
|
||||||
|
class="tab-button py-2 px-1 border-b-2 font-medium text-sm">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0-1.125.504-1.125 1.125V11.25a9 9 0 00-9-9z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Launcher C#
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -191,7 +202,7 @@
|
||||||
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
|
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
|
||||||
Salvar
|
Salvar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Add directory history dropdown -->
|
<!-- Add directory history dropdown -->
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
|
@ -232,7 +243,7 @@
|
||||||
<!-- Launcher Controls -->
|
<!-- Launcher Controls -->
|
||||||
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="text-xl font-bold">Launcher GUI - Scripts Independientes</h2>
|
<h2 class="text-xl font-bold">Launcher GUI - Scripts Python Independientes</h2>
|
||||||
<button onclick="openGroupEditor()"
|
<button onclick="openGroupEditor()"
|
||||||
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
||||||
Gestionar Grupos
|
Gestionar Grupos
|
||||||
|
@ -367,6 +378,125 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content: Launcher C# -->
|
||||||
|
<div id="csharp-content" class="tab-content hidden">
|
||||||
|
<!-- C# Project Controls -->
|
||||||
|
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold">Launcher C# - Proyectos Compilados</h2>
|
||||||
|
<button onclick="openCSharpProjectEditor()"
|
||||||
|
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
||||||
|
Gestionar Proyectos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Selector -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-2">Seleccionar Proyecto C#</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<select id="csharp-project-select" class="w-full p-3 border rounded-lg pl-12"
|
||||||
|
onchange="loadCSharpExecutables()">
|
||||||
|
<option value="">-- Seleccionar Proyecto --</option>
|
||||||
|
</select>
|
||||||
|
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
|
||||||
|
<div id="selected-csharp-project-icon"
|
||||||
|
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">🗂️
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onclick="openCSharpProjectInEditor('cursor')"
|
||||||
|
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
|
||||||
|
id="cursor-csharp-btn" style="display: none;" title="Abrir proyecto en Cursor">
|
||||||
|
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
|
||||||
|
alt="Cursor Icon">
|
||||||
|
</button>
|
||||||
|
<button onclick="openCSharpProjectInEditor('vs2022')"
|
||||||
|
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
|
||||||
|
id="vs2022-csharp-btn" style="display: none;" title="Abrir proyecto en Visual Studio 2022">
|
||||||
|
💜
|
||||||
|
</button>
|
||||||
|
<button onclick="openCSharpProjectFolder()"
|
||||||
|
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
|
||||||
|
id="folder-csharp-btn" style="display: none;" title="Abrir carpeta del proyecto">
|
||||||
|
📁
|
||||||
|
</button>
|
||||||
|
<button onclick="copyCSharpProjectPath()"
|
||||||
|
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
|
||||||
|
id="copy-path-csharp-btn" style="display: none;" title="Copiar path del proyecto">
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Filter -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-sm font-medium mb-2">Filtrar por Categoría</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button class="csharp-category-btn active px-3 py-1 rounded-full text-sm border"
|
||||||
|
data-category="all" onclick="filterCSharpByCategory('all')">
|
||||||
|
Todas
|
||||||
|
</button>
|
||||||
|
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border"
|
||||||
|
data-category="Aplicaciones" onclick="filterCSharpByCategory('Aplicaciones')">
|
||||||
|
🖥️ Aplicaciones
|
||||||
|
</button>
|
||||||
|
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border"
|
||||||
|
data-category="Herramientas" onclick="filterCSharpByCategory('Herramientas')">
|
||||||
|
🔧 Herramientas
|
||||||
|
</button>
|
||||||
|
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border" data-category="APIs"
|
||||||
|
onclick="filterCSharpByCategory('APIs')">
|
||||||
|
🌐 APIs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- C# Favorites Panel -->
|
||||||
|
<div id="csharp-favorites-panel" class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="text-lg font-semibold text-blue-800">
|
||||||
|
⭐ Ejecutables Favoritos
|
||||||
|
</h3>
|
||||||
|
<span class="text-sm text-blue-600" id="csharp-favorites-count">
|
||||||
|
0 favoritos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="csharp-favorites-list" class="space-y-2">
|
||||||
|
<!-- Lista dinámica de favoritos C# -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- C# Executables Grid -->
|
||||||
|
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-bold">Ejecutables Disponibles</h2>
|
||||||
|
<button onclick="openCSharpExecutableManager()"
|
||||||
|
class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600"
|
||||||
|
id="manage-csharp-executables-btn" style="display: none;">
|
||||||
|
Gestionar Ejecutables
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="csharp-executables-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<!-- Executable cards dinámicos -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Running Processes Panel -->
|
||||||
|
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">🔄 Procesos en Ejecución</h3>
|
||||||
|
<button onclick="refreshCSharpProcesses()" class="text-blue-500 hover:text-blue-700 text-sm">
|
||||||
|
Actualizar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="csharp-running-processes" class="space-y-2">
|
||||||
|
<!-- Lista dinámica de procesos -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Logs (común para ambos sistemas) -->
|
<!-- Logs (común para ambos sistemas) -->
|
||||||
<div class="bg-white p-6 rounded-lg shadow">
|
<div class="bg-white p-6 rounded-lg shadow">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
@ -783,6 +913,7 @@
|
||||||
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
||||||
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/csharp_launcher.js') }}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
// Inicializar markdown-it globalmente
|
// Inicializar markdown-it globalmente
|
||||||
window.markdownit = window.markdownit || markdownit;
|
window.markdownit = window.markdownit || markdownit;
|
||||||
|
|
Loading…
Reference in New Issue