Creacion de la AEditor Basicamente un Editor solo para el bot
feat: Add new commands for Discord bot - Implemented "Everyone" command that replies to the user when executed. - Added "sdfsdfsdf" command with an alias "dfsf" that also replies to the user. - Enhanced the command structure with type definitions for better type safety.
This commit is contained in:
722
AEditor/src/App.vue
Normal file
722
AEditor/src/App.vue
Normal file
@@ -0,0 +1,722 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import Sidebar from "./components/Sidebar.vue";
|
||||
import MonacoEditor from "./components/MonacoEditor.vue";
|
||||
import CommandCreator from "./components/CommandCreator.vue";
|
||||
import EventCreator from "./components/EventCreator.vue";
|
||||
import ProjectSelector from "./components/ProjectSelector.vue";
|
||||
import CommandPalette from "./components/CommandPalette.vue";
|
||||
import SkeletonLoader from "./components/SkeletonLoader.vue";
|
||||
import DatabaseViewer from "./components/DatabaseViewer.vue";
|
||||
import EnvManager from "./components/EnvManager.vue";
|
||||
import type { ProjectStats, FileInfo, Command, Event } from "./types/bot";
|
||||
|
||||
// Estado de la aplicación
|
||||
const projectRoot = ref<string>("");
|
||||
const showProjectSelector = ref(false);
|
||||
const showCommandPalette = ref(false);
|
||||
const devUltraMode = ref(false);
|
||||
const initialLoading = ref(true);
|
||||
const stats = ref<ProjectStats>({
|
||||
messageCommands: 0,
|
||||
slashCommands: 0,
|
||||
standardEvents: 0,
|
||||
customEvents: 0,
|
||||
totalCommands: 0,
|
||||
totalEvents: 0,
|
||||
});
|
||||
const commands = ref<FileInfo[]>([]);
|
||||
const events = ref<FileInfo[]>([]);
|
||||
const allFiles = ref<FileInfo[]>([]);
|
||||
const selectedFile = ref<FileInfo | null>(null);
|
||||
const fileContent = ref<string>("");
|
||||
const currentView = ref<"editor" | "command-creator" | "event-creator" | "database" | "env-manager">("editor");
|
||||
const loading = ref(false);
|
||||
const errorMsg = ref<string>("");
|
||||
const schemaContent = ref<string>("");
|
||||
|
||||
// Deshabilitar F12 y habilitar Ctrl+Q
|
||||
const handleF12 = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F12') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCtrlQ = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'q') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCommandPalette.value = !showCommandPalette.value;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleF12);
|
||||
window.addEventListener('keydown', handleCtrlQ);
|
||||
|
||||
// Inicializar Discord RPC
|
||||
initDiscordRPC();
|
||||
|
||||
// Cargar proyecto inicial
|
||||
loadProjectData();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleF12);
|
||||
window.removeEventListener('keydown', handleCtrlQ);
|
||||
|
||||
// Desconectar Discord RPC al cerrar
|
||||
disconnectDiscordRPC();
|
||||
});
|
||||
|
||||
// Funciones Discord RPC
|
||||
async function initDiscordRPC() {
|
||||
try {
|
||||
await invoke('init_discord_rpc');
|
||||
console.log('✅ Discord RPC inicializado');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ No se pudo inicializar Discord RPC:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateDiscordRPC(details: string, state: string, fileName?: string) {
|
||||
try {
|
||||
await invoke('update_discord_rpc', { details, state, fileName });
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Error actualizando Discord RPC:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectDiscordRPC() {
|
||||
try {
|
||||
await invoke('disconnect_discord_rpc');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Error desconectando Discord RPC:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar datos del proyecto
|
||||
async function loadProjectData() {
|
||||
loading.value = true;
|
||||
errorMsg.value = "";
|
||||
|
||||
try {
|
||||
// Si no hay projectRoot, intentar cargar desde localStorage o mostrar selector
|
||||
if (!projectRoot.value) {
|
||||
const savedPath = localStorage.getItem('amayo-project-path');
|
||||
if (savedPath) {
|
||||
// Validar ruta guardada
|
||||
const isValid = await invoke<boolean>('validate_project_path', {
|
||||
path: savedPath,
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
projectRoot.value = savedPath;
|
||||
} else {
|
||||
// Ruta inválida, mostrar selector
|
||||
showProjectSelector.value = true;
|
||||
initialLoading.value = false;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No hay ruta guardada, mostrar selector
|
||||
showProjectSelector.value = true;
|
||||
initialLoading.value = false;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar estadísticas
|
||||
stats.value = await invoke<ProjectStats>("get_project_stats", {
|
||||
projectRoot: projectRoot.value
|
||||
});
|
||||
console.log("📊 Stats cargadas:", stats.value);
|
||||
|
||||
// Cargar comandos
|
||||
commands.value = await invoke<FileInfo[]>("scan_commands", {
|
||||
projectRoot: projectRoot.value
|
||||
});
|
||||
console.log("💬 Comandos cargados:", commands.value.length, commands.value);
|
||||
|
||||
// Cargar eventos
|
||||
events.value = await invoke<FileInfo[]>("scan_events", {
|
||||
projectRoot: projectRoot.value
|
||||
});
|
||||
console.log("📡 Eventos cargados:", events.value.length, events.value);
|
||||
|
||||
// Cargar schema.prisma si existe
|
||||
try {
|
||||
schemaContent.value = await invoke<string>("read_file_content", {
|
||||
filePath: `${projectRoot.value}/prisma/schema.prisma`
|
||||
});
|
||||
} catch {
|
||||
schemaContent.value = "// Schema no encontrado";
|
||||
}
|
||||
|
||||
// Actualizar Discord RPC
|
||||
updateDiscordRPC(
|
||||
"Navegando proyecto",
|
||||
`${stats.value.totalCommands} comandos | ${stats.value.totalEvents} eventos`
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error cargando proyecto: ${error}`;
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
// Pequeño delay para mostrar el skeleton
|
||||
setTimeout(() => {
|
||||
initialLoading.value = false;
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
|
||||
// Seleccionar archivo
|
||||
async function selectFile(file: FileInfo) {
|
||||
try {
|
||||
selectedFile.value = file;
|
||||
fileContent.value = await invoke<string>("read_file_content", {
|
||||
filePath: file.path
|
||||
});
|
||||
currentView.value = "editor";
|
||||
|
||||
// Actualizar Discord RPC
|
||||
const fileType = file.commandType
|
||||
? `Comando ${file.commandType}`
|
||||
: file.eventType
|
||||
? `Evento ${file.eventType}`
|
||||
: "Archivo";
|
||||
updateDiscordRPC(`Editando ${fileType}`, file.name);
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error leyendo archivo: ${error}`;
|
||||
console.error("Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar archivo
|
||||
async function saveFile(content: string) {
|
||||
if (!selectedFile.value) return;
|
||||
|
||||
try {
|
||||
await invoke("write_file_content", {
|
||||
filePath: selectedFile.value.path,
|
||||
content: content
|
||||
});
|
||||
|
||||
// Mostrar notificación de éxito
|
||||
showNotification("✅ Archivo guardado correctamente");
|
||||
|
||||
// Recargar estadísticas
|
||||
await loadProjectData();
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error guardando archivo: ${error}`;
|
||||
console.error("Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Crear nuevo comando
|
||||
function showCommandCreator() {
|
||||
currentView.value = "command-creator";
|
||||
updateDiscordRPC("Creando comando nuevo", "En el wizard de comandos");
|
||||
}
|
||||
|
||||
// Crear nuevo evento
|
||||
function showEventCreator() {
|
||||
currentView.value = "event-creator";
|
||||
updateDiscordRPC("Creando evento nuevo", "En el wizard de eventos");
|
||||
}
|
||||
|
||||
// Guardar nuevo comando
|
||||
async function saveCommand(_command: Command, code: string, savePath: string) {
|
||||
try {
|
||||
const fullPath = `${projectRoot.value}/${savePath}`;
|
||||
await invoke("write_file_content", {
|
||||
filePath: fullPath,
|
||||
content: code
|
||||
});
|
||||
|
||||
showNotification("✅ Comando creado correctamente");
|
||||
currentView.value = "editor";
|
||||
await loadProjectData();
|
||||
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error creando comando: ${error}`;
|
||||
console.error("Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar nuevo evento
|
||||
async function saveEvent(_event: Event, code: string, savePath: string) {
|
||||
try {
|
||||
const fullPath = `${projectRoot.value}/${savePath}`;
|
||||
await invoke("write_file_content", {
|
||||
filePath: fullPath,
|
||||
content: code
|
||||
});
|
||||
|
||||
showNotification("✅ Evento creado correctamente");
|
||||
currentView.value = "editor";
|
||||
await loadProjectData();
|
||||
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error creando evento: ${error}`;
|
||||
console.error("Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cerrar creadores
|
||||
function closeCreator() {
|
||||
currentView.value = "editor";
|
||||
}
|
||||
|
||||
// Mostrar notificación temporal
|
||||
function showNotification(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Manejar selección de proyecto
|
||||
function handleProjectPathSelected(path: string) {
|
||||
projectRoot.value = path;
|
||||
showProjectSelector.value = false;
|
||||
loadProjectData();
|
||||
}
|
||||
|
||||
// Cambiar directorio del proyecto
|
||||
function changeProjectDirectory() {
|
||||
showProjectSelector.value = true;
|
||||
}
|
||||
|
||||
// Toggle modo Dev Ultra
|
||||
async function toggleDevUltra() {
|
||||
devUltraMode.value = !devUltraMode.value;
|
||||
if (devUltraMode.value) {
|
||||
showNotification("⚡ Modo Dev Ultra Activado - Cargando archivos...");
|
||||
// Cargar todos los archivos del proyecto
|
||||
try {
|
||||
allFiles.value = await invoke<FileInfo[]>("scan_all_files", {
|
||||
projectRoot: projectRoot.value
|
||||
});
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error cargando archivos: ${error}`;
|
||||
console.error("Error:", error);
|
||||
}
|
||||
} else {
|
||||
showNotification("🔒 Modo Dev Ultra Desactivado");
|
||||
allFiles.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle Database Viewer
|
||||
function toggleDatabase() {
|
||||
if (currentView.value === 'database') {
|
||||
currentView.value = 'editor';
|
||||
updateDiscordRPC("Navegando proyecto", "En el editor");
|
||||
} else {
|
||||
currentView.value = 'database';
|
||||
updateDiscordRPC("Editando base de datos", "Visualizando schema.prisma");
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle Env Manager
|
||||
function toggleEnvManager() {
|
||||
if (currentView.value === 'env-manager') {
|
||||
currentView.value = 'editor';
|
||||
updateDiscordRPC("Navegando proyecto", "En el editor");
|
||||
} else {
|
||||
currentView.value = 'env-manager';
|
||||
updateDiscordRPC("Configurando variables", "Gestionando .env");
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar schema de base de datos
|
||||
async function saveSchema(content: string) {
|
||||
try {
|
||||
await invoke("write_file_content", {
|
||||
filePath: `${projectRoot.value}/prisma/schema.prisma`,
|
||||
content: content
|
||||
});
|
||||
schemaContent.value = content;
|
||||
showNotification("✅ Schema guardado correctamente");
|
||||
} catch (error: any) {
|
||||
errorMsg.value = `Error guardando schema: ${error}`;
|
||||
console.error("Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Manejar comandos del palette
|
||||
function handlePaletteCommand(commandId: string) {
|
||||
switch (commandId) {
|
||||
case 'new-command':
|
||||
showCommandCreator();
|
||||
break;
|
||||
case 'new-event':
|
||||
showEventCreator();
|
||||
break;
|
||||
case 'refresh':
|
||||
loadProjectData();
|
||||
break;
|
||||
case 'change-project':
|
||||
changeProjectDirectory();
|
||||
break;
|
||||
case 'database':
|
||||
toggleDatabase();
|
||||
break;
|
||||
case 'env-manager':
|
||||
toggleEnvManager();
|
||||
break;
|
||||
case 'toggle-dev-ultra':
|
||||
toggleDevUltra();
|
||||
break;
|
||||
case 'save':
|
||||
if (selectedFile.value && fileContent.value) {
|
||||
saveFile(fileContent.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- Skeleton Loader -->
|
||||
<SkeletonLoader v-if="initialLoading" />
|
||||
|
||||
<!-- Command Palette -->
|
||||
<CommandPalette
|
||||
:isOpen="showCommandPalette"
|
||||
@close="showCommandPalette = false"
|
||||
@command="handlePaletteCommand"
|
||||
/>
|
||||
|
||||
<!-- Selector de proyecto -->
|
||||
<ProjectSelector
|
||||
v-if="showProjectSelector && !initialLoading"
|
||||
@path-selected="handleProjectPathSelected"
|
||||
/>
|
||||
|
||||
<template v-if="!showProjectSelector && !initialLoading">
|
||||
<Sidebar
|
||||
:stats="stats"
|
||||
:commands="commands"
|
||||
:events="events"
|
||||
:allFiles="allFiles"
|
||||
:selectedFile="selectedFile"
|
||||
:projectRoot="projectRoot"
|
||||
:devUltraMode="devUltraMode"
|
||||
@new-command="showCommandCreator"
|
||||
@new-event="showEventCreator"
|
||||
@refresh="loadProjectData"
|
||||
@select-file="selectFile"
|
||||
@change-directory="changeProjectDirectory"
|
||||
@toggle-dev-ultra="toggleDevUltra"
|
||||
@toggle-database="toggleDatabase"
|
||||
@toggle-env-manager="toggleEnvManager"
|
||||
@notify="showNotification"
|
||||
/>
|
||||
|
||||
<div class="main-content">
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-spinner">⏳ Cargando proyecto...</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMsg" class="error-banner">
|
||||
❌ {{ errorMsg }}
|
||||
<button @click="errorMsg = ''" class="close-error">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor Monaco -->
|
||||
<MonacoEditor
|
||||
v-if="currentView === 'editor' && selectedFile"
|
||||
:fileInfo="selectedFile"
|
||||
:content="fileContent"
|
||||
@save="saveFile"
|
||||
/>
|
||||
|
||||
<!-- Command Creator -->
|
||||
<CommandCreator
|
||||
v-if="currentView === 'command-creator'"
|
||||
@save="saveCommand"
|
||||
@close="closeCreator"
|
||||
/>
|
||||
|
||||
<!-- Event Creator -->
|
||||
<EventCreator
|
||||
v-if="currentView === 'event-creator'"
|
||||
@save="saveEvent"
|
||||
@close="closeCreator"
|
||||
/>
|
||||
|
||||
<!-- Database Viewer -->
|
||||
<DatabaseViewer
|
||||
v-if="currentView === 'database'"
|
||||
:schemaContent="schemaContent"
|
||||
:projectRoot="projectRoot"
|
||||
@save="saveSchema"
|
||||
/>
|
||||
|
||||
<!-- Environment Manager -->
|
||||
<EnvManager
|
||||
v-if="currentView === 'env-manager'"
|
||||
:projectRoot="projectRoot"
|
||||
@close="() => currentView = 'editor'"
|
||||
@notify="showNotification"
|
||||
/>
|
||||
|
||||
<!-- Welcome Screen -->
|
||||
<div v-if="currentView === 'editor' && !selectedFile" class="welcome-screen">
|
||||
<div class="welcome-content">
|
||||
<h1>🤖 Amayo Bot Editor</h1>
|
||||
<p>Editor estilo VS Code para tu bot de Discord</p>
|
||||
<div class="welcome-stats">
|
||||
<div class="welcome-stat">
|
||||
<div class="stat-number">{{ stats.totalCommands }}</div>
|
||||
<div class="stat-label">Comandos Totales</div>
|
||||
</div>
|
||||
<div class="welcome-stat">
|
||||
<div class="stat-number">{{ stats.totalEvents }}</div>
|
||||
<div class="stat-label">Eventos Totales</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<button @click="showCommandCreator" class="welcome-btn primary">
|
||||
➕ Crear Comando
|
||||
</button>
|
||||
<button @click="showEventCreator" class="welcome-btn primary">
|
||||
➕ Crear Evento
|
||||
</button>
|
||||
</div>
|
||||
<p class="welcome-hint">
|
||||
💡 <strong>Tip:</strong> Selecciona un archivo del panel izquierdo para editarlo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(30, 30, 30, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
background-color: #f14c4c;
|
||||
color: #ffffff;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-error {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-error:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.welcome-screen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.welcome-content h1 {
|
||||
color: #ffffff;
|
||||
font-size: 48px;
|
||||
margin: 0 0 16px 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.welcome-content > p {
|
||||
color: #cccccc;
|
||||
font-size: 18px;
|
||||
margin: 0 0 40px 0;
|
||||
}
|
||||
|
||||
.welcome-stats {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
justify-content: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.welcome-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #4ec9b0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #cccccc;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.welcome-btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.welcome-btn.primary {
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.welcome-btn.primary:hover {
|
||||
background-color: #1177bb;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.welcome-hint {
|
||||
color: #858585;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background-color: #252526;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #0e639c;
|
||||
}
|
||||
|
||||
.welcome-hint strong {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Notificaciones */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
color: #ffffff;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background-color: #4ec9b0;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background-color: #f48771;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background-color: #007acc;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
AEditor/src/assets/vue.svg
Normal file
1
AEditor/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
713
AEditor/src/components/CommandCreator.vue
Normal file
713
AEditor/src/components/CommandCreator.vue
Normal file
@@ -0,0 +1,713 @@
|
||||
<template>
|
||||
<div class="command-creator">
|
||||
<div class="creator-header">
|
||||
<h2>{{ isEditing ? 'Editar Comando' : 'Crear Nuevo Comando' }}</h2>
|
||||
<button @click="$emit('close')" class="close-btn">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="creator-content">
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label>Tipo de Comando *</label>
|
||||
<select v-model="commandData.type">
|
||||
<option value="message">Comando por Mensaje (prefix)</option>
|
||||
<option value="slash">Comando Slash (/comando)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nombre del Comando *</label>
|
||||
<input
|
||||
v-model="commandData.name"
|
||||
type="text"
|
||||
placeholder="ping, help, user-info..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Descripción *</label>
|
||||
<input
|
||||
v-model="commandData.description"
|
||||
type="text"
|
||||
placeholder="Describe qué hace este comando"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Categoría</label>
|
||||
<input
|
||||
v-model="commandData.category"
|
||||
type="text"
|
||||
placeholder="Utilidad, Diversión, Admin..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Cooldown (segundos)</label>
|
||||
<input
|
||||
v-model.number="commandData.cooldown"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="commandData.type === 'message'" class="form-group">
|
||||
<label>Aliases (separados por coma)</label>
|
||||
<input
|
||||
v-model="aliasesInput"
|
||||
type="text"
|
||||
placeholder="p, pong, latencia"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="commandData.type === 'message'" class="form-group">
|
||||
<label>Uso</label>
|
||||
<input
|
||||
v-model="commandData.usage"
|
||||
type="text"
|
||||
placeholder="!comando [arg1] [arg2]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Ruta de Guardado *</label>
|
||||
<input
|
||||
v-model="commandData.savePath"
|
||||
type="text"
|
||||
:placeholder="getDefaultPath()"
|
||||
/>
|
||||
<small>La ruta relativa donde se guardará el archivo .ts</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-section">
|
||||
<div class="editor-header-small">
|
||||
<h3>Función run() - Lógica del Comando</h3>
|
||||
</div>
|
||||
<div class="monaco-wrapper" ref="editorContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="creator-footer">
|
||||
<button @click="$emit('close')" class="cancel-btn">Cancelar</button>
|
||||
<button @click="saveCommand" :disabled="!isValid" class="save-btn">
|
||||
{{ isEditing ? '💾 Guardar Cambios' : '➕ Crear Comando' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import type { Command } from '../types/bot';
|
||||
|
||||
const props = defineProps<{
|
||||
initialCommand?: Command;
|
||||
isEditing?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'save': [command: Command, code: string, savePath: string];
|
||||
'close': [];
|
||||
}>();
|
||||
|
||||
const commandData = ref({
|
||||
name: '',
|
||||
type: 'message' as 'message' | 'slash',
|
||||
description: '',
|
||||
category: '',
|
||||
cooldown: 0,
|
||||
usage: '',
|
||||
savePath: '',
|
||||
});
|
||||
|
||||
const aliasesInput = ref('');
|
||||
const editorContainer = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||
|
||||
const isValid = computed(() => {
|
||||
return commandData.value.name.trim() !== '' &&
|
||||
commandData.value.description.trim() !== '' &&
|
||||
commandData.value.savePath.trim() !== '';
|
||||
});
|
||||
|
||||
function getDefaultPath(): string {
|
||||
const type = commandData.value.type === 'slash' ? 'splashcmd' : 'messages';
|
||||
const category = commandData.value.category?.toLowerCase() || 'others';
|
||||
return `src/commands/${type}/${category}/${commandData.value.name}.ts`;
|
||||
}
|
||||
|
||||
watch(() => commandData.value, () => {
|
||||
if (!commandData.value.savePath || commandData.value.savePath === '') {
|
||||
commandData.value.savePath = getDefaultPath();
|
||||
}
|
||||
|
||||
// Actualizar el editor en tiempo real con los cambios del formulario
|
||||
if (editor) {
|
||||
const currentPosition = editor.getPosition();
|
||||
const newCode = getDefaultCode();
|
||||
editor.setValue(newCode);
|
||||
if (currentPosition) {
|
||||
editor.setPosition(currentPosition);
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Watch para aliases que también actualiza el editor
|
||||
watch(() => aliasesInput.value, () => {
|
||||
if (editor) {
|
||||
const currentPosition = editor.getPosition();
|
||||
const newCode = getDefaultCode();
|
||||
editor.setValue(newCode);
|
||||
if (currentPosition) {
|
||||
editor.setPosition(currentPosition);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getDefaultCode(): string {
|
||||
if (commandData.value.type === 'slash') {
|
||||
return `import type { ChatInputCommandInteraction } from "discord.js";
|
||||
import type Amayo from "../../core/client";
|
||||
|
||||
export default {
|
||||
name: "${commandData.value.name}",
|
||||
description: "${commandData.value.description}",
|
||||
type: 'slash' as const,
|
||||
${commandData.value.cooldown > 0 ? `cooldown: ${commandData.value.cooldown},\n ` : ''}async run(interaction: ChatInputCommandInteraction, client: Amayo) {
|
||||
// Tu código aquí
|
||||
await interaction.reply({
|
||||
content: "¡Comando ${commandData.value.name} ejecutado!",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}`;
|
||||
} else {
|
||||
return `import type { Message } from "discord.js";
|
||||
import type Amayo from "../../core/client";
|
||||
|
||||
export default {
|
||||
name: "${commandData.value.name}",
|
||||
description: "${commandData.value.description}",
|
||||
type: 'message' as const,
|
||||
${commandData.value.category ? `category: "${commandData.value.category}",\n ` : ''}${commandData.value.usage ? `usage: "${commandData.value.usage}",\n ` : ''}${aliasesInput.value ? `aliases: [${aliasesInput.value.split(',').map(a => `"${a.trim()}"`).join(', ')}],\n ` : ''}${commandData.value.cooldown > 0 ? `cooldown: ${commandData.value.cooldown},\n ` : ''}async run(message: Message, args: string[], client: Amayo) {
|
||||
// Tu código aquí
|
||||
await message.reply("¡Comando ${commandData.value.name} ejecutado!");
|
||||
}
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCommand() {
|
||||
if (!isValid.value || !editor) return;
|
||||
|
||||
const code = editor.getValue();
|
||||
const aliases = aliasesInput.value
|
||||
.split(',')
|
||||
.map(a => a.trim())
|
||||
.filter(a => a !== '');
|
||||
|
||||
const command: Command = {
|
||||
name: commandData.value.name,
|
||||
type: commandData.value.type,
|
||||
description: commandData.value.description,
|
||||
cooldown: commandData.value.cooldown > 0 ? commandData.value.cooldown : undefined,
|
||||
run: code,
|
||||
} as Command;
|
||||
|
||||
if (commandData.value.type === 'message') {
|
||||
(command as any).category = commandData.value.category || undefined;
|
||||
(command as any).usage = commandData.value.usage || undefined;
|
||||
if (aliases.length > 0) {
|
||||
(command as any).aliases = aliases;
|
||||
}
|
||||
}
|
||||
|
||||
emit('save', command, code, commandData.value.savePath);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (editorContainer.value) {
|
||||
monaco.editor.defineTheme('vs-dark-custom', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#1e1e1e',
|
||||
}
|
||||
});
|
||||
|
||||
const initialCode = props.initialCommand?.run || getDefaultCode();
|
||||
|
||||
editor = monaco.editor.create(editorContainer.value, {
|
||||
value: initialCode,
|
||||
language: 'typescript',
|
||||
theme: 'vs-dark-custom',
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: false,
|
||||
scrollBeyondLastLine: false,
|
||||
tabSize: 2,
|
||||
suggestOnTriggerCharacters: true,
|
||||
quickSuggestions: {
|
||||
other: true,
|
||||
comments: false,
|
||||
strings: true
|
||||
},
|
||||
});
|
||||
|
||||
// Configurar autocompletado para Discord.js + TypeScript snippets nativos
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||
noEmit: true,
|
||||
esModuleInterop: true,
|
||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||
allowJs: true,
|
||||
strict: true,
|
||||
noImplicitAny: true,
|
||||
strictNullChecks: true,
|
||||
strictFunctionTypes: true,
|
||||
});
|
||||
|
||||
// Habilitar snippets de TypeScript nativos y Discord.js
|
||||
monaco.languages.registerCompletionItemProvider('typescript', {
|
||||
provideCompletionItems: (model, position) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
};
|
||||
|
||||
const suggestions: monaco.languages.CompletionItem[] = [
|
||||
{
|
||||
label: 'try-catch',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'try {',
|
||||
'\t${1:// Tu código aquí}',
|
||||
'} catch (error) {',
|
||||
'\tconsole.error(error);',
|
||||
'\t${2:// Manejo de error}',
|
||||
'}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Try-catch block',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'async-function',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'async function ${1:name}(${2:params}) {',
|
||||
'\t${3:// Código asíncrono}',
|
||||
'}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Async function declaration',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'discord-embed',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'embeds: [{',
|
||||
'\ttitle: "${1:Título}",',
|
||||
'\tdescription: "${2:Descripción}",',
|
||||
'\tcolor: 0x${3:0099ff},',
|
||||
'\tfields: [',
|
||||
'\t\t{ name: "${4:Campo}", value: "${5:Valor}", inline: ${6:true} }',
|
||||
'\t],',
|
||||
'\ttimestamp: new Date(),',
|
||||
'\tfooter: { text: "${7:Footer}" }',
|
||||
'}]'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Discord embed structure',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'message-reply',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'await message.reply("${1:Mensaje}");',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Responder a un mensaje',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'interaction-reply',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'await interaction.reply({ content: "${1:Mensaje}", ephemeral: ${2:true} });',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Responder a una interacción slash',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'interaction-defer',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'await interaction.deferReply({ ephemeral: ${1:true} });',
|
||||
'${2:// Tu código largo aquí}',
|
||||
'await interaction.editReply({ content: "${3:¡Listo!}" });'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Diferir respuesta de interacción',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'logger-info',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'logger.info("${1:Mensaje de log}");',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Log de información',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'logger-error',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'logger.error({ err: ${1:error} }, "${2:Mensaje de error}");',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Log de error con contexto',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'prisma-findUnique',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'const ${1:result} = await prisma.${2:model}.findUnique({',
|
||||
'\twhere: { ${3:id}: ${4:value} }',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Buscar registro único en Prisma',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'prisma-create',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'const ${1:result} = await prisma.${2:model}.create({',
|
||||
'\tdata: {',
|
||||
'\t\t${3:field}: ${4:value}',
|
||||
'\t}',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Crear registro en Prisma',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'prisma-update',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'const ${1:result} = await prisma.${2:model}.update({',
|
||||
'\twhere: { ${3:id}: ${4:value} },',
|
||||
'\tdata: {',
|
||||
'\t\t${5:field}: ${6:newValue}',
|
||||
'\t}',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Actualizar registro en Prisma',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'check-permissions',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'if (!message.member?.permissions.has("${1:Administrator}")) {',
|
||||
'\treturn message.reply("❌ No tienes permisos.");',
|
||||
'}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Verificar permisos del usuario',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'check-args',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'if (args.length < ${1:1}) {',
|
||||
'\treturn message.reply("❌ Uso: ${2:!comando <arg>}");',
|
||||
'}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Verificar argumentos del comando',
|
||||
range
|
||||
}
|
||||
];
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar datos si está editando
|
||||
if (props.initialCommand) {
|
||||
commandData.value = {
|
||||
name: props.initialCommand.name,
|
||||
type: props.initialCommand.type,
|
||||
description: props.initialCommand.description || '',
|
||||
category: (props.initialCommand as any).category || '',
|
||||
cooldown: props.initialCommand.cooldown || 0,
|
||||
usage: (props.initialCommand as any).usage || '',
|
||||
savePath: '',
|
||||
};
|
||||
|
||||
if (props.initialCommand.type === 'message') {
|
||||
aliasesInput.value = ((props.initialCommand as any).aliases || []).join(', ');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (editor) {
|
||||
editor.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.command-creator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #1e1e1e;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.creator-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background-color: #2d2d30;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.creator-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #ffffff;
|
||||
background-color: #3e3e42;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.creator-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
width: 400px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.code-preview {
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background-color: #2d2d30;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.preview-badge {
|
||||
font-size: 10px;
|
||||
padding: 3px 8px;
|
||||
background-color: #4ec9b0;
|
||||
color: #1e1e1e;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preview-code {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background-color: #1e1e1e;
|
||||
overflow-x: auto;
|
||||
max-height: 200px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.preview-code code {
|
||||
white-space: pre;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: #3c3c3c;
|
||||
border: 1px solid #3e3e42;
|
||||
color: #cccccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #0e639c;
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.editor-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-header-small {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.editor-header-small h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.monaco-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.creator-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background-color: #2d2d30;
|
||||
border-top: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.save-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #3e3e42;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: #4e4e52;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.save-btn:hover:not(:disabled) {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.save-btn:disabled {
|
||||
background-color: #3e3e42;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.form-section::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.form-section::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.form-section::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
330
AEditor/src/components/CommandPalette.vue
Normal file
330
AEditor/src/components/CommandPalette.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="palette-overlay" @click="close">
|
||||
<div class="palette-container" @click.stop>
|
||||
<div class="palette-header">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Buscar comandos (Ctrl+Q)..."
|
||||
class="palette-search"
|
||||
@keydown.down.prevent="navigateDown"
|
||||
@keydown.up.prevent="navigateUp"
|
||||
@keydown.enter.prevent="executeSelected"
|
||||
@keydown.esc="close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="palette-results">
|
||||
<div
|
||||
v-for="(cmd, index) in filteredCommands"
|
||||
:key="cmd.id"
|
||||
:class="['palette-item', { active: index === selectedIndex }]"
|
||||
@click="execute(cmd)"
|
||||
@mouseenter="selectedIndex = index"
|
||||
>
|
||||
<span class="palette-icon">{{ cmd.icon }}</span>
|
||||
<div class="palette-info">
|
||||
<div class="palette-name">{{ cmd.name }}</div>
|
||||
<div class="palette-desc">{{ cmd.description }}</div>
|
||||
</div>
|
||||
<kbd v-if="cmd.shortcut" class="palette-shortcut">{{ cmd.shortcut }}</kbd>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredCommands.length === 0" class="palette-empty">
|
||||
No se encontraron comandos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
|
||||
interface PaletteCommand {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
shortcut?: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'close': [];
|
||||
'command': [commandId: string];
|
||||
}>();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const selectedIndex = ref(0);
|
||||
const searchInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Comandos disponibles (se pasarán desde App.vue)
|
||||
const commands = ref<PaletteCommand[]>([
|
||||
{
|
||||
id: 'new-command',
|
||||
name: 'Crear Nuevo Comando',
|
||||
description: 'Crear un comando de mensaje o slash',
|
||||
icon: '➕',
|
||||
action: () => emit('command', 'new-command')
|
||||
},
|
||||
{
|
||||
id: 'new-event',
|
||||
name: 'Crear Nuevo Evento',
|
||||
description: 'Crear un manejador de eventos',
|
||||
icon: '⚡',
|
||||
action: () => emit('command', 'new-event')
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
name: 'Actualizar Proyecto',
|
||||
description: 'Recargar estadísticas y archivos',
|
||||
icon: '🔄',
|
||||
shortcut: 'Ctrl+R',
|
||||
action: () => emit('command', 'refresh')
|
||||
},
|
||||
{
|
||||
id: 'change-project',
|
||||
name: 'Cambiar Proyecto',
|
||||
description: 'Seleccionar otro directorio',
|
||||
icon: '📁',
|
||||
action: () => emit('command', 'change-project')
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
name: 'Ver Base de Datos',
|
||||
description: 'Abrir visor de Prisma schema',
|
||||
icon: '🗄️',
|
||||
action: () => emit('command', 'database')
|
||||
},
|
||||
{
|
||||
id: 'toggle-dev-ultra',
|
||||
name: 'Modo Dev Ultra',
|
||||
description: 'Habilitar edición completa del src/',
|
||||
icon: '⚡',
|
||||
action: () => emit('command', 'toggle-dev-ultra')
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
name: 'Guardar Archivo',
|
||||
description: 'Guardar el archivo actual',
|
||||
icon: '💾',
|
||||
shortcut: 'Ctrl+S',
|
||||
action: () => emit('command', 'save')
|
||||
},
|
||||
]);
|
||||
|
||||
const filteredCommands = computed(() => {
|
||||
if (!searchQuery.value) return commands.value;
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return commands.value.filter(cmd =>
|
||||
cmd.name.toLowerCase().includes(query) ||
|
||||
cmd.description.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
function navigateDown() {
|
||||
if (selectedIndex.value < filteredCommands.value.length - 1) {
|
||||
selectedIndex.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
if (selectedIndex.value > 0) {
|
||||
selectedIndex.value--;
|
||||
}
|
||||
}
|
||||
|
||||
function executeSelected() {
|
||||
const cmd = filteredCommands.value[selectedIndex.value];
|
||||
if (cmd) {
|
||||
execute(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
function execute(cmd: PaletteCommand) {
|
||||
cmd.action();
|
||||
close();
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close');
|
||||
searchQuery.value = '';
|
||||
selectedIndex.value = 0;
|
||||
}
|
||||
|
||||
// Focus en el input cuando se abre
|
||||
watch(() => props.isOpen, async (isOpen) => {
|
||||
if (isOpen) {
|
||||
await nextTick();
|
||||
searchInput.value?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Atajos de teclado globales
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'q') {
|
||||
e.preventDefault();
|
||||
if (props.isOpen) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.palette-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(2px);
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-container {
|
||||
width: 600px;
|
||||
max-height: 500px;
|
||||
background-color: #252526;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.palette-search {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background-color: #3c3c3c;
|
||||
border: 1px solid #3e3e42;
|
||||
color: #cccccc;
|
||||
font-size: 15px;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.palette-search:focus {
|
||||
border-color: #0e639c;
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.palette-results {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #2d2d30;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.palette-item:hover,
|
||||
.palette-item.active {
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.palette-icon {
|
||||
font-size: 20px;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.palette-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.palette-name {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.palette-desc {
|
||||
color: #858585;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.palette-shortcut {
|
||||
background-color: #3c3c3c;
|
||||
color: #cccccc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
border: 1px solid #4e4e52;
|
||||
}
|
||||
|
||||
.palette-empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #858585;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.palette-results::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.palette-results::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.palette-results::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
359
AEditor/src/components/DatabaseViewer.vue
Normal file
359
AEditor/src/components/DatabaseViewer.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="db-viewer">
|
||||
<div class="db-tabs">
|
||||
<button
|
||||
:class="['db-tab', { active: currentTab === 'schema' }]"
|
||||
@click="currentTab = 'schema'"
|
||||
>
|
||||
📝 Schema
|
||||
</button>
|
||||
<button
|
||||
:class="['db-tab', { active: currentTab === 'diagram' }]"
|
||||
@click="currentTab = 'diagram'"
|
||||
>
|
||||
🗺️ Diagrama
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Vista del Schema (Editor Monaco) - Usar v-show para mantenerlo montado -->
|
||||
<div v-show="currentTab === 'schema'" class="schema-editor">
|
||||
<div ref="schemaEditorContainer" class="monaco-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Vista del Diagrama (Visual) -->
|
||||
<div v-show="currentTab === 'diagram'" class="diagram-view">
|
||||
<div class="diagram-canvas" ref="diagramCanvas">
|
||||
<div
|
||||
v-for="table in tables"
|
||||
:key="table.name"
|
||||
class="table-card"
|
||||
>
|
||||
<div class="table-header">
|
||||
<span class="table-icon">🗃️</span>
|
||||
<span class="table-name">{{ table.name }}</span>
|
||||
</div>
|
||||
<div class="table-fields">
|
||||
<div v-for="field in table.fields" :key="field.name" class="table-field">
|
||||
<span :class="['field-icon', { primary: field.isPrimary }]">
|
||||
{{ field.isPrimary ? '🔑' : '●' }}
|
||||
</span>
|
||||
<span class="field-name">{{ field.name }}</span>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="table.relations.length > 0" class="table-relations">
|
||||
<div class="relations-title">🔗 Relaciones</div>
|
||||
<div v-for="rel in table.relations" :key="rel" class="relation-item">
|
||||
{{ rel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
interface TableField {
|
||||
name: string;
|
||||
type: string;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
interface Table {
|
||||
name: string;
|
||||
fields: TableField[];
|
||||
relations: string[];
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
schemaContent: string;
|
||||
projectRoot: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'save': [content: string];
|
||||
}>();
|
||||
|
||||
const currentTab = ref<'schema' | 'diagram'>('schema');
|
||||
const schemaEditorContainer = ref<HTMLElement | null>(null);
|
||||
let schemaEditor: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||
|
||||
const tables = ref<Table[]>([]);
|
||||
|
||||
// Watch para actualizar el editor cuando cambie schemaContent
|
||||
watch(() => props.schemaContent, (newContent) => {
|
||||
if (schemaEditor && schemaEditor.getValue() !== newContent) {
|
||||
schemaEditor.setValue(newContent);
|
||||
}
|
||||
parseSchema();
|
||||
});
|
||||
|
||||
// Parsear el schema de Prisma para extraer tablas
|
||||
function parseSchema() {
|
||||
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
||||
const fieldRegex = /(\w+)\s+([\w\[\]?]+)(?:.*@id)?/g;
|
||||
|
||||
const parsedTables: Table[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = modelRegex.exec(props.schemaContent)) !== null) {
|
||||
const tableName = match[1];
|
||||
const tableBody = match[2];
|
||||
|
||||
const fields: TableField[] = [];
|
||||
const relations: string[] = [];
|
||||
|
||||
const fieldMatches = tableBody.matchAll(fieldRegex);
|
||||
for (const fieldMatch of fieldMatches) {
|
||||
const fieldName = fieldMatch[1];
|
||||
const fieldType = fieldMatch[2];
|
||||
const isPrimary = tableBody.includes(`${fieldName}.*@id`);
|
||||
|
||||
if (!['model', 'enum'].includes(fieldName)) {
|
||||
fields.push({
|
||||
name: fieldName,
|
||||
type: fieldType,
|
||||
isPrimary
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar relaciones
|
||||
const relationMatches = tableBody.matchAll(/(\w+)\s+(\w+)(?:\[\])?/g);
|
||||
for (const relMatch of relationMatches) {
|
||||
const relType = relMatch[2];
|
||||
if (relType[0] === relType[0].toUpperCase() && relType !== 'String' && relType !== 'Int' && relType !== 'Boolean' && relType !== 'DateTime' && relType !== 'Json') {
|
||||
if (!relations.includes(relType)) {
|
||||
relations.push(relType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedTables.push({
|
||||
name: tableName,
|
||||
fields: fields,
|
||||
relations,
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
}
|
||||
|
||||
tables.value = parsedTables;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Crear editor Monaco para el schema
|
||||
if (schemaEditorContainer.value) {
|
||||
schemaEditor = monaco.editor.create(schemaEditorContainer.value, {
|
||||
value: props.schemaContent,
|
||||
language: 'prisma',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: true },
|
||||
fontSize: 13,
|
||||
lineNumbers: 'on',
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
// Guardar con Ctrl+S
|
||||
schemaEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||
if (schemaEditor) {
|
||||
emit('save', schemaEditor.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Parsear el schema para el diagrama
|
||||
parseSchema();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (schemaEditor) {
|
||||
schemaEditor.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.db-tabs {
|
||||
display: flex;
|
||||
background-color: #2d2d30;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.db-tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.db-tab:hover {
|
||||
background-color: #3e3e42;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.db-tab.active {
|
||||
color: #ffffff;
|
||||
border-bottom-color: #0e639c;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.schema-editor {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.diagram-view {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background-color: #1e1e1e;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.diagram-canvas {
|
||||
position: relative;
|
||||
min-width: max-content;
|
||||
min-height: max-content;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
width: 240px;
|
||||
background-color: #1f1f1f;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: linear-gradient(135deg, #2d2d30 0%, #252526 100%);
|
||||
border-bottom: 2px solid #007acc;
|
||||
}
|
||||
|
||||
.table-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.table-name {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.table-fields {
|
||||
padding: 8px 0;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.table-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.table-field:hover {
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
font-size: 8px;
|
||||
opacity: 0.7;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.field-icon.primary {
|
||||
opacity: 1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
flex: 1;
|
||||
color: #d4d4d4;
|
||||
font-size: 12px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-type {
|
||||
color: #4ec9b0;
|
||||
font-size: 11px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-relations {
|
||||
padding: 10px 12px;
|
||||
background-color: #252526;
|
||||
border-top: 1px solid #3e3e42;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.relations-title {
|
||||
color: #d7ba7d;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.relation-item {
|
||||
color: #d4d4d4;
|
||||
font-size: 11px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: #1e1e1e;
|
||||
border-left: 3px solid #007acc;
|
||||
border-radius: 2px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
</style>
|
||||
1022
AEditor/src/components/EnvManager.vue
Normal file
1022
AEditor/src/components/EnvManager.vue
Normal file
File diff suppressed because it is too large
Load Diff
635
AEditor/src/components/EventCreator.vue
Normal file
635
AEditor/src/components/EventCreator.vue
Normal file
@@ -0,0 +1,635 @@
|
||||
<template>
|
||||
<div class="event-creator">
|
||||
<div class="creator-header">
|
||||
<h2>{{ isEditing ? 'Editar Evento' : 'Crear Nuevo Evento' }}</h2>
|
||||
<button @click="$emit('close')" class="close-btn">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="creator-content">
|
||||
<div class="form-section">
|
||||
<div class="form-group">
|
||||
<label>Tipo de Evento *</label>
|
||||
<select v-model="eventData.eventType">
|
||||
<option value="standard">Evento Estándar de Discord.js</option>
|
||||
<option value="extra">Evento Custom/Extra</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="eventData.eventType === 'standard'" class="form-group">
|
||||
<label>Evento de Discord *</label>
|
||||
<select v-model="eventData.discordEvent">
|
||||
<option value="">Selecciona un evento...</option>
|
||||
<option value="ready">ready - Bot está listo</option>
|
||||
<option value="messageCreate">messageCreate - Nuevo mensaje</option>
|
||||
<option value="interactionCreate">interactionCreate - Interacción recibida</option>
|
||||
<option value="guildCreate">guildCreate - Bot añadido a servidor</option>
|
||||
<option value="guildDelete">guildDelete - Bot removido de servidor</option>
|
||||
<option value="guildMemberAdd">guildMemberAdd - Miembro se une</option>
|
||||
<option value="guildMemberRemove">guildMemberRemove - Miembro sale</option>
|
||||
<option value="messageDelete">messageDelete - Mensaje eliminado</option>
|
||||
<option value="messageUpdate">messageUpdate - Mensaje editado</option>
|
||||
<option value="channelCreate">channelCreate - Canal creado</option>
|
||||
<option value="channelDelete">channelDelete - Canal eliminado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nombre del Archivo *</label>
|
||||
<input
|
||||
v-model="eventData.fileName"
|
||||
type="text"
|
||||
placeholder="myCustomEvent, allianceHandler..."
|
||||
/>
|
||||
<small>Nombre del archivo .ts (sin extensión)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Descripción</label>
|
||||
<textarea
|
||||
v-model="eventData.description"
|
||||
rows="3"
|
||||
placeholder="Describe qué hace este evento..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Ruta de Guardado *</label>
|
||||
<input
|
||||
v-model="eventData.savePath"
|
||||
type="text"
|
||||
:placeholder="getDefaultPath()"
|
||||
/>
|
||||
<small>La ruta relativa donde se guardará el archivo .ts</small>
|
||||
</div>
|
||||
|
||||
<div v-if="eventData.eventType === 'extra'" class="info-box">
|
||||
<p><strong>ℹ️ Eventos Custom/Extra:</strong></p>
|
||||
<p>Los eventos extras son funciones que se ejecutan dentro de eventos estándar (como messageCreate).
|
||||
Deben exportar una función que será llamada desde el evento principal.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-section">
|
||||
<div class="editor-header-small">
|
||||
<h3>Código del Evento</h3>
|
||||
</div>
|
||||
<div class="monaco-wrapper" ref="editorContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="creator-footer">
|
||||
<button @click="$emit('close')" class="cancel-btn">Cancelar</button>
|
||||
<button @click="saveEvent" :disabled="!isValid" class="save-btn">
|
||||
{{ isEditing ? '💾 Guardar Cambios' : '➕ Crear Evento' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import type { Event } from '../types/bot';
|
||||
|
||||
const props = defineProps<{
|
||||
initialEvent?: Event;
|
||||
isEditing?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'save': [event: Event, code: string, savePath: string];
|
||||
'close': [];
|
||||
}>();
|
||||
|
||||
const eventData = ref({
|
||||
fileName: '',
|
||||
eventType: 'standard' as 'standard' | 'extra',
|
||||
discordEvent: '',
|
||||
description: '',
|
||||
savePath: '',
|
||||
});
|
||||
|
||||
const editorContainer = ref<HTMLElement | null>(null);
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||
|
||||
const isValid = computed(() => {
|
||||
if (eventData.value.eventType === 'standard') {
|
||||
return eventData.value.fileName.trim() !== '' &&
|
||||
eventData.value.discordEvent.trim() !== '' &&
|
||||
eventData.value.savePath.trim() !== '';
|
||||
}
|
||||
return eventData.value.fileName.trim() !== '' &&
|
||||
eventData.value.savePath.trim() !== '';
|
||||
});
|
||||
|
||||
function getDefaultPath(): string {
|
||||
if (eventData.value.eventType === 'extra') {
|
||||
return `src/events/extras/${eventData.value.fileName}.ts`;
|
||||
}
|
||||
return `src/events/${eventData.value.fileName || eventData.value.discordEvent}.ts`;
|
||||
}
|
||||
|
||||
watch(() => eventData.value, () => {
|
||||
if (!eventData.value.savePath || eventData.value.savePath === '') {
|
||||
eventData.value.savePath = getDefaultPath();
|
||||
}
|
||||
|
||||
// Actualizar el editor en tiempo real con los cambios del formulario
|
||||
if (editor) {
|
||||
const currentPosition = editor.getPosition();
|
||||
const newCode = getDefaultCode();
|
||||
editor.setValue(newCode);
|
||||
if (currentPosition) {
|
||||
editor.setPosition(currentPosition);
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
function getDefaultCode(): string {
|
||||
if (eventData.value.eventType === 'extra') {
|
||||
return `import { Message } from "discord.js";
|
||||
import logger from "../../core/lib/logger";
|
||||
|
||||
/**
|
||||
* ${eventData.value.description || 'Función custom que se ejecuta desde messageCreate'}
|
||||
*/
|
||||
export async function ${eventData.value.fileName || 'customHandler'}(message: Message) {
|
||||
try {
|
||||
// Verificar condiciones
|
||||
if (message.author.bot) return;
|
||||
|
||||
// Tu lógica aquí
|
||||
logger.info(\`Evento custom ejecutado para mensaje de \${message.author.tag}\`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Error en evento custom");
|
||||
}
|
||||
}
|
||||
`;
|
||||
} else {
|
||||
const eventName = eventData.value.discordEvent || 'ready';
|
||||
const templates: Record<string, string> = {
|
||||
ready: `import { bot } from "../main";
|
||||
import { Events } from "discord.js";
|
||||
import logger from "../core/lib/logger";
|
||||
|
||||
bot.on(Events.ClientReady, async (client) => {
|
||||
logger.info(\`✅ Bot iniciado como \${client.user.tag}\`);
|
||||
|
||||
// Tu código aquí
|
||||
});
|
||||
`,
|
||||
messageCreate: `import { bot } from "../main";
|
||||
import { Events } from "discord.js";
|
||||
import logger from "../core/lib/logger";
|
||||
|
||||
bot.on(Events.MessageCreate, async (message) => {
|
||||
if (message.author.bot) return;
|
||||
|
||||
// Tu código aquí
|
||||
logger.info(\`Mensaje recibido de \${message.author.tag}\`);
|
||||
});
|
||||
`,
|
||||
interactionCreate: `import { bot } from "../main";
|
||||
import { Events } from "discord.js";
|
||||
import logger from "../core/lib/logger";
|
||||
|
||||
bot.on(Events.InteractionCreate, async (interaction) => {
|
||||
// Tu código aquí
|
||||
if (interaction.isChatInputCommand()) {
|
||||
logger.info(\`Comando slash: \${interaction.commandName}\`);
|
||||
}
|
||||
});
|
||||
`,
|
||||
};
|
||||
|
||||
return templates[eventName] || `import { bot } from "../main";
|
||||
import { Events } from "discord.js";
|
||||
import logger from "../core/lib/logger";
|
||||
|
||||
bot.on(Events.${eventName.charAt(0).toUpperCase() + eventName.slice(1)}, async (...args) => {
|
||||
// Tu código aquí
|
||||
logger.info("Evento ${eventName} ejecutado");
|
||||
});
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function saveEvent() {
|
||||
if (!isValid.value || !editor) return;
|
||||
|
||||
const code = editor.getValue();
|
||||
|
||||
const event: Event = {
|
||||
name: eventData.value.fileName,
|
||||
type: eventData.value.eventType,
|
||||
eventName: eventData.value.eventType === 'standard' ? eventData.value.discordEvent : undefined,
|
||||
path: eventData.value.savePath,
|
||||
code: code,
|
||||
};
|
||||
|
||||
emit('save', event, code, eventData.value.savePath);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (editorContainer.value) {
|
||||
monaco.editor.defineTheme('vs-dark-custom', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#1e1e1e',
|
||||
}
|
||||
});
|
||||
|
||||
const initialCode = props.initialEvent?.code || getDefaultCode();
|
||||
|
||||
editor = monaco.editor.create(editorContainer.value, {
|
||||
value: initialCode,
|
||||
language: 'typescript',
|
||||
theme: 'vs-dark-custom',
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: false,
|
||||
scrollBeyondLastLine: false,
|
||||
tabSize: 2,
|
||||
suggestOnTriggerCharacters: true,
|
||||
quickSuggestions: {
|
||||
other: true,
|
||||
comments: false,
|
||||
strings: true
|
||||
},
|
||||
});
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||
noEmit: true,
|
||||
esModuleInterop: true,
|
||||
strict: true,
|
||||
noImplicitAny: true,
|
||||
strictNullChecks: true,
|
||||
strictFunctionTypes: true,
|
||||
});
|
||||
|
||||
// Registrar snippets personalizados para eventos
|
||||
monaco.languages.registerCompletionItemProvider('typescript', {
|
||||
provideCompletionItems: (model, position) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
};
|
||||
|
||||
const suggestions: monaco.languages.CompletionItem[] = [
|
||||
{
|
||||
label: 'try-catch',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'try {',
|
||||
'\t${1:// Tu código aquí}',
|
||||
'} catch (error) {',
|
||||
'\tlogger.error({ err: error }, "${2:Error en evento}");',
|
||||
'}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Try-catch block con logger',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'logger-info',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'logger.info("${1:Mensaje de log}");',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Log de información',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'logger-error',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'logger.error({ err: ${1:error} }, "${2:Mensaje de error}");',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Log de error con contexto',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'check-bot-message',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'if (message.author.bot) return;',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Ignorar mensajes de bots',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'check-guild',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'if (!message.guild) return;',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Verificar si está en un servidor',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'check-content',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'if (!message.content || message.content.trim() === "") return;',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Verificar contenido del mensaje',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'prisma-findUnique',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'const ${1:result} = await prisma.${2:model}.findUnique({',
|
||||
'\twhere: { ${3:id}: ${4:value} }',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Buscar registro único en Prisma',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'prisma-create',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'const ${1:result} = await prisma.${2:model}.create({',
|
||||
'\tdata: {',
|
||||
'\t\t${3:field}: ${4:value}',
|
||||
'\t}',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Crear registro en Prisma',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'discord-embed',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'const embed = {',
|
||||
'\ttitle: "${1:Título}",',
|
||||
'\tdescription: "${2:Descripción}",',
|
||||
'\tcolor: 0x${3:0099ff},',
|
||||
'\tfields: [',
|
||||
'\t\t{ name: "${4:Campo}", value: "${5:Valor}", inline: ${6:true} }',
|
||||
'\t],',
|
||||
'\ttimestamp: new Date(),',
|
||||
'\tfooter: { text: "${7:Footer}" }',
|
||||
'};'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Embed de Discord',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'event-ready',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'bot.on(Events.ClientReady, async (client) => {',
|
||||
'\tlogger.info(`✅ Bot iniciado como \\${client.user.tag}`);',
|
||||
'\t${1:// Tu código aquí}',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Evento ready de Discord',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'event-messageCreate',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'bot.on(Events.MessageCreate, async (message) => {',
|
||||
'\tif (message.author.bot) return;',
|
||||
'\t${1:// Tu código aquí}',
|
||||
'});'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Evento messageCreate de Discord',
|
||||
range
|
||||
}
|
||||
];
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (props.initialEvent) {
|
||||
eventData.value = {
|
||||
fileName: props.initialEvent.name,
|
||||
eventType: props.initialEvent.type,
|
||||
discordEvent: props.initialEvent.eventName || '',
|
||||
description: '',
|
||||
savePath: props.initialEvent.path,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (editor) {
|
||||
editor.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.event-creator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #1e1e1e;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.creator-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background-color: #2d2d30;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.creator-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #ffffff;
|
||||
background-color: #3e3e42;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.creator-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
width: 400px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: #3c3c3c;
|
||||
border: 1px solid #3e3e42;
|
||||
color: #cccccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #0e639c;
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 12px;
|
||||
background-color: #2d2d30;
|
||||
border-left: 3px solid #0e639c;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.info-box p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.editor-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-header-small {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.editor-header-small h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.monaco-wrapper {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.creator-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background-color: #2d2d30;
|
||||
border-top: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.save-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #3e3e42;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: #4e4e52;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.save-btn:hover:not(:disabled) {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.save-btn:disabled {
|
||||
background-color: #3e3e42;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-section::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.form-section::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.form-section::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
841
AEditor/src/components/FileExplorer.vue
Normal file
841
AEditor/src/components/FileExplorer.vue
Normal file
@@ -0,0 +1,841 @@
|
||||
<template>
|
||||
<div class="file-explorer">
|
||||
<!-- Context Menu -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@click.stop
|
||||
>
|
||||
<div class="context-menu-item" @click="showNewFileModal">
|
||||
<span class="menu-icon">📄</span>
|
||||
<span>Nuevo Archivo</span>
|
||||
<span class="menu-shortcut">Ctrl+N</span>
|
||||
</div>
|
||||
<div class="context-menu-item" @click="showNewFolderModal">
|
||||
<span class="menu-icon">📁</span>
|
||||
<span>Nueva Carpeta</span>
|
||||
<span class="menu-shortcut">Ctrl+Shift+N</span>
|
||||
</div>
|
||||
<div v-if="contextMenu.target" class="context-menu-divider"></div>
|
||||
<div
|
||||
v-if="contextMenu.target"
|
||||
class="context-menu-item"
|
||||
@click="showRenameModal"
|
||||
>
|
||||
<span class="menu-icon">✏️</span>
|
||||
<span>Renombrar</span>
|
||||
<span class="menu-shortcut">F2</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="contextMenu.target"
|
||||
class="context-menu-item danger"
|
||||
@click="showDeleteConfirmation"
|
||||
>
|
||||
<span class="menu-icon">🗑️</span>
|
||||
<span>Eliminar</span>
|
||||
<span class="menu-shortcut">Del</span>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Modal: Nuevo Archivo/Carpeta/Renombrar -->
|
||||
<Teleport to="body">
|
||||
<div v-if="inputModal.visible" class="modal-overlay" @click="closeInputModal">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>{{ inputModal.title }}</h3>
|
||||
<button class="modal-close" @click="closeInputModal">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="input-label">{{ inputModal.label }}</label>
|
||||
<input
|
||||
ref="inputField"
|
||||
v-model="inputModal.value"
|
||||
type="text"
|
||||
class="input-field"
|
||||
:placeholder="inputModal.placeholder"
|
||||
@keydown.enter="handleInputSubmit"
|
||||
@keydown.esc="closeInputModal"
|
||||
/>
|
||||
<div v-if="inputModal.error" class="input-error">
|
||||
{{ inputModal.error }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="closeInputModal">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="handleInputSubmit"
|
||||
:disabled="!inputModal.value.trim()"
|
||||
>
|
||||
{{ inputModal.confirmText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Modal: Confirmación de Eliminación -->
|
||||
<Teleport to="body">
|
||||
<div v-if="deleteModal.visible" class="modal-overlay" @click="closeDeleteModal">
|
||||
<div class="modal-content modal-danger" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>⚠️ Confirmar Eliminación</h3>
|
||||
<button class="modal-close" @click="closeDeleteModal">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>¿Estás seguro de que deseas eliminar?</p>
|
||||
<div class="delete-target">
|
||||
<span class="file-icon">{{ deleteModal.isFolder ? '📁' : '📄' }}</span>
|
||||
<strong>{{ deleteModal.targetName }}</strong>
|
||||
</div>
|
||||
<p class="warning-text">
|
||||
Esta acción no se puede deshacer.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="closeDeleteModal">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- File Tree -->
|
||||
<div class="file-tree" @click="closeContextMenu">
|
||||
<template v-for="(files, folder) in groupedFiles" :key="folder">
|
||||
<!-- Folder Header -->
|
||||
<div
|
||||
v-if="folder !== 'root'"
|
||||
class="folder-item"
|
||||
@contextmenu.prevent="openContextMenu($event, null, folder)"
|
||||
>
|
||||
<div class="folder-header">
|
||||
<span class="folder-icon">📁</span>
|
||||
<span class="folder-name">{{ folder }}</span>
|
||||
<span class="file-count">{{ files.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files in Folder -->
|
||||
<div
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
:class="['file-item', {
|
||||
active: selectedFile?.path === file.path,
|
||||
'in-folder': folder !== 'root'
|
||||
}]"
|
||||
@click="$emit('select-file', file)"
|
||||
@contextmenu.prevent="openContextMenu($event, file, folder)"
|
||||
>
|
||||
<span class="file-icon">{{ getFileIcon(file) }}</span>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="Object.keys(groupedFiles).length === 0" class="empty-state">
|
||||
<p>Sin archivos</p>
|
||||
<p class="empty-hint">Click derecho para crear archivos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch } from 'vue';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { FileInfo } from '../types/bot';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
files: FileInfo[];
|
||||
selectedFile: FileInfo | null;
|
||||
projectRoot: string;
|
||||
basePath: string; // 'commands' | 'events' | '' (para root)
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'select-file': [file: FileInfo];
|
||||
'refresh': [];
|
||||
'notify': [message: string, type: 'success' | 'error' | 'info'];
|
||||
}>();
|
||||
|
||||
// State: Context Menu
|
||||
const contextMenu = ref({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
target: null as FileInfo | null,
|
||||
folder: '' as string
|
||||
});
|
||||
|
||||
// State: Input Modal (Nuevo/Renombrar)
|
||||
const inputModal = ref({
|
||||
visible: false,
|
||||
type: '' as 'file' | 'folder' | 'rename',
|
||||
title: '',
|
||||
label: '',
|
||||
placeholder: '',
|
||||
confirmText: '',
|
||||
value: '',
|
||||
error: '',
|
||||
targetFile: null as FileInfo | null,
|
||||
targetFolder: ''
|
||||
});
|
||||
|
||||
// State: Delete Modal
|
||||
const deleteModal = ref({
|
||||
visible: false,
|
||||
target: null as FileInfo | null,
|
||||
targetName: '',
|
||||
isFolder: false
|
||||
});
|
||||
|
||||
const inputField = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Computed: Agrupar archivos por carpeta
|
||||
const groupedFiles = computed(() => {
|
||||
const grouped: Record<string, FileInfo[]> = {};
|
||||
|
||||
console.log(`🗂️ FileExplorer recibió ${props.files.length} archivos para basePath="${props.basePath}"`);
|
||||
|
||||
props.files.forEach(file => {
|
||||
const folder = file.folder || 'root';
|
||||
if (!grouped[folder]) {
|
||||
grouped[folder] = [];
|
||||
}
|
||||
grouped[folder].push(file);
|
||||
});
|
||||
|
||||
console.log("📁 Grupos creados:", Object.keys(grouped), grouped);
|
||||
|
||||
return grouped;
|
||||
});
|
||||
|
||||
// Context Menu
|
||||
function openContextMenu(event: MouseEvent, file: FileInfo | null, folder: string) {
|
||||
contextMenu.value = {
|
||||
visible: true,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
target: file,
|
||||
folder: folder
|
||||
};
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
contextMenu.value.visible = false;
|
||||
}
|
||||
|
||||
// Cerrar context menu al hacer click fuera
|
||||
watch(() => contextMenu.value.visible, (visible) => {
|
||||
if (visible) {
|
||||
const handleClickOutside = () => {
|
||||
closeContextMenu();
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Modales: Nuevo Archivo
|
||||
function showNewFileModal() {
|
||||
inputModal.value = {
|
||||
visible: true,
|
||||
type: 'file',
|
||||
title: '📄 Nuevo Archivo',
|
||||
label: 'Nombre del archivo:',
|
||||
placeholder: 'ejemplo.ts',
|
||||
confirmText: 'Crear',
|
||||
value: '',
|
||||
error: '',
|
||||
targetFile: null,
|
||||
targetFolder: contextMenu.value.folder
|
||||
};
|
||||
closeContextMenu();
|
||||
nextTick(() => inputField.value?.focus());
|
||||
}
|
||||
|
||||
// Modales: Nueva Carpeta
|
||||
function showNewFolderModal() {
|
||||
inputModal.value = {
|
||||
visible: true,
|
||||
type: 'folder',
|
||||
title: '📁 Nueva Carpeta',
|
||||
label: 'Nombre de la carpeta:',
|
||||
placeholder: 'mi-carpeta',
|
||||
confirmText: 'Crear',
|
||||
value: '',
|
||||
error: '',
|
||||
targetFile: null,
|
||||
targetFolder: contextMenu.value.folder
|
||||
};
|
||||
closeContextMenu();
|
||||
nextTick(() => inputField.value?.focus());
|
||||
}
|
||||
|
||||
// Modales: Renombrar
|
||||
function showRenameModal() {
|
||||
const target = contextMenu.value.target;
|
||||
if (!target) return;
|
||||
|
||||
inputModal.value = {
|
||||
visible: true,
|
||||
type: 'rename',
|
||||
title: '✏️ Renombrar',
|
||||
label: 'Nuevo nombre:',
|
||||
placeholder: target.name,
|
||||
confirmText: 'Renombrar',
|
||||
value: target.name,
|
||||
error: '',
|
||||
targetFile: target,
|
||||
targetFolder: contextMenu.value.folder
|
||||
};
|
||||
closeContextMenu();
|
||||
nextTick(() => {
|
||||
if (inputField.value) {
|
||||
inputField.value.focus();
|
||||
// Seleccionar el nombre sin la extensión
|
||||
const dotIndex = inputModal.value.value.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
inputField.value.setSelectionRange(0, dotIndex);
|
||||
} else {
|
||||
inputField.value.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeInputModal() {
|
||||
inputModal.value.visible = false;
|
||||
inputModal.value.value = '';
|
||||
inputModal.value.error = '';
|
||||
}
|
||||
|
||||
// Modales: Eliminar
|
||||
function showDeleteConfirmation() {
|
||||
const target = contextMenu.value.target;
|
||||
if (!target) return;
|
||||
|
||||
deleteModal.value = {
|
||||
visible: true,
|
||||
target: target,
|
||||
targetName: target.name,
|
||||
isFolder: false // TODO: Detectar si es carpeta
|
||||
};
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
deleteModal.value.visible = false;
|
||||
}
|
||||
|
||||
// Handlers: Crear/Renombrar
|
||||
async function handleInputSubmit() {
|
||||
const { type, value, targetFile, targetFolder } = inputModal.value;
|
||||
|
||||
if (!value.trim()) {
|
||||
inputModal.value.error = 'El nombre no puede estar vacío';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar caracteres inválidos
|
||||
const invalidChars = /[<>:"/\\|?*]/;
|
||||
if (invalidChars.test(value)) {
|
||||
inputModal.value.error = 'Nombre contiene caracteres inválidos';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (type === 'file') {
|
||||
await handleCreateFile(value, targetFolder);
|
||||
} else if (type === 'folder') {
|
||||
await handleCreateFolder(value, targetFolder);
|
||||
} else if (type === 'rename' && targetFile) {
|
||||
await handleRename(targetFile, value);
|
||||
}
|
||||
|
||||
closeInputModal();
|
||||
} catch (error: any) {
|
||||
inputModal.value.error = error.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Backend Calls
|
||||
async function handleCreateFile(filename: string, folder: string) {
|
||||
try {
|
||||
// Construir la ruta completa
|
||||
let fullPath = props.projectRoot;
|
||||
if (props.basePath) {
|
||||
fullPath += '/' + props.basePath;
|
||||
}
|
||||
if (folder !== 'root') {
|
||||
fullPath += '/' + folder;
|
||||
}
|
||||
fullPath += '/' + filename;
|
||||
|
||||
await invoke('create_file', { filePath: fullPath });
|
||||
|
||||
emit('notify', `✅ Archivo "${filename}" creado correctamente`, 'success');
|
||||
emit('refresh');
|
||||
} catch (error: any) {
|
||||
emit('notify', `❌ Error creando archivo: ${error}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateFolder(folderName: string, parentFolder: string) {
|
||||
try {
|
||||
// Construir la ruta completa
|
||||
let fullPath = props.projectRoot;
|
||||
if (props.basePath) {
|
||||
fullPath += '/' + props.basePath;
|
||||
}
|
||||
if (parentFolder !== 'root') {
|
||||
fullPath += '/' + parentFolder;
|
||||
}
|
||||
fullPath += '/' + folderName;
|
||||
|
||||
await invoke('create_folder', { folderPath: fullPath });
|
||||
|
||||
emit('notify', `✅ Carpeta "${folderName}" creada correctamente`, 'success');
|
||||
emit('refresh');
|
||||
} catch (error: any) {
|
||||
emit('notify', `❌ Error creando carpeta: ${error}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRename(file: FileInfo, newName: string) {
|
||||
try {
|
||||
const oldPath = `${props.projectRoot}/${file.relative_path}`;
|
||||
const dirPath = file.relative_path.substring(0, file.relative_path.lastIndexOf('/'));
|
||||
const newPath = `${props.projectRoot}/${dirPath}/${newName}`;
|
||||
|
||||
await invoke('rename_file', { oldPath, newPath });
|
||||
|
||||
emit('notify', `✅ Renombrado a "${newName}" correctamente`, 'success');
|
||||
emit('refresh');
|
||||
} catch (error: any) {
|
||||
emit('notify', `❌ Error renombrando: ${error}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
const target = deleteModal.value.target;
|
||||
if (!target) return;
|
||||
|
||||
try {
|
||||
const fullPath = `${props.projectRoot}/${target.relative_path}`;
|
||||
|
||||
if (deleteModal.value.isFolder) {
|
||||
await invoke('delete_folder', { folderPath: fullPath });
|
||||
} else {
|
||||
await invoke('delete_file', { filePath: fullPath });
|
||||
}
|
||||
|
||||
emit('notify', `✅ Eliminado correctamente`, 'success');
|
||||
emit('refresh');
|
||||
closeDeleteModal();
|
||||
} catch (error: any) {
|
||||
emit('notify', `❌ Error eliminando: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Utilidades
|
||||
function getFileIcon(file: FileInfo): string {
|
||||
if (file.commandType === 'message') return '💬';
|
||||
if (file.commandType === 'slash') return '⚡';
|
||||
if (file.eventType === 'standard') return '📡';
|
||||
if (file.eventType === 'extra') return '🎯';
|
||||
|
||||
// Por extensión
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'ts': return '🔷';
|
||||
case 'js': return '🟨';
|
||||
case 'json': return '📋';
|
||||
case 'md': return '📝';
|
||||
default: return '📄';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-explorer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: #2d2d30;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
padding: 4px;
|
||||
min-width: 220px;
|
||||
z-index: 10000;
|
||||
animation: contextMenuSlideIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes contextMenuSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #cccccc;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: #37373d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.context-menu-item.danger:hover {
|
||||
background: #5a1d1d;
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu-shortcut {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
background: #3e3e42;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #2d2d30;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
width: 450px;
|
||||
max-width: 90vw;
|
||||
animation: modalSlideUp 0.25s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-danger {
|
||||
border-color: #be1100;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #858585;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #37373d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
margin: 0 0 16px 0;
|
||||
color: #cccccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
margin-top: 8px;
|
||||
color: #f48771;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.delete-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #be1100;
|
||||
border-radius: 4px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.delete-target strong {
|
||||
color: #f48771;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #f48771;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0e639c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #1177bb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #3c3c3c;
|
||||
color: #cccccc;
|
||||
border: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4e4e52;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #be1100;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #f14c2e;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* File Tree */
|
||||
.file-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.folder-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
background: #252526;
|
||||
border-left: 3px solid #007acc;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.folder-header:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.file-count {
|
||||
font-size: 11px;
|
||||
color: #858585;
|
||||
background: #1e1e1e;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #2a2d2e;
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
background-color: #37373d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.file-item.in-folder {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid #3e3e42;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 8px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 11px;
|
||||
color: #6a6a6a;
|
||||
}
|
||||
</style>
|
||||
1680
AEditor/src/components/MonacoEditor.vue
Normal file
1680
AEditor/src/components/MonacoEditor.vue
Normal file
File diff suppressed because it is too large
Load Diff
326
AEditor/src/components/ProjectSelector.vue
Normal file
326
AEditor/src/components/ProjectSelector.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div class="project-selector-overlay">
|
||||
<div class="project-selector-modal">
|
||||
<div class="modal-header">
|
||||
<h2>🚀 Selecciona el Directorio del Proyecto Amayo</h2>
|
||||
<p>Elige la carpeta raíz donde está el proyecto del bot</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
❌ {{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div class="path-display">
|
||||
<div class="path-label">Ruta actual:</div>
|
||||
<div class="path-value">{{ currentPath || 'No seleccionada' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="selectDirectory" class="select-btn">
|
||||
📁 Seleccionar Directorio
|
||||
</button>
|
||||
<button
|
||||
v-if="currentPath"
|
||||
@click="validateAndSave"
|
||||
class="save-btn"
|
||||
:disabled="validating"
|
||||
>
|
||||
{{ validating ? '⏳ Validando...' : '✅ Usar esta Ruta' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>ℹ️ Requisitos:</strong></p>
|
||||
<ul>
|
||||
<li>El directorio debe contener <code>src/commands/</code></li>
|
||||
<li>El directorio debe contener <code>src/events/</code></li>
|
||||
<li>Compatible con Windows, Linux y macOS</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="savedPath" class="saved-path">
|
||||
<p><strong>📌 Última ruta guardada:</strong></p>
|
||||
<p class="saved-value">{{ savedPath }}</p>
|
||||
<button @click="useSavedPath" class="use-saved-btn">
|
||||
Usar Ruta Guardada
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
const emit = defineEmits<{
|
||||
'path-selected': [path: string];
|
||||
}>();
|
||||
|
||||
const currentPath = ref<string>('');
|
||||
const savedPath = ref<string>('');
|
||||
const errorMessage = ref<string>('');
|
||||
const validating = ref(false);
|
||||
|
||||
// Cargar ruta guardada
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem('amayo-project-path');
|
||||
if (saved) {
|
||||
savedPath.value = saved;
|
||||
}
|
||||
});
|
||||
|
||||
// Seleccionar directorio
|
||||
async function selectDirectory() {
|
||||
try {
|
||||
errorMessage.value = '';
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Selecciona el directorio raíz del proyecto Amayo',
|
||||
});
|
||||
|
||||
if (selected && typeof selected === 'string') {
|
||||
currentPath.value = selected;
|
||||
}
|
||||
} catch (error: any) {
|
||||
errorMessage.value = `Error al seleccionar directorio: ${error.message}`;
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Validar y guardar
|
||||
async function validateAndSave() {
|
||||
if (!currentPath.value) {
|
||||
errorMessage.value = 'Por favor selecciona un directorio primero';
|
||||
return;
|
||||
}
|
||||
|
||||
validating.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
const isValid = await invoke<boolean>('validate_project_path', {
|
||||
path: currentPath.value,
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
// Guardar en localStorage
|
||||
localStorage.setItem('amayo-project-path', currentPath.value);
|
||||
|
||||
// Emitir evento
|
||||
emit('path-selected', currentPath.value);
|
||||
} else {
|
||||
errorMessage.value =
|
||||
'El directorio seleccionado no es válido. ' +
|
||||
'Asegúrate de que contiene las carpetas src/commands/ y src/events/';
|
||||
}
|
||||
} catch (error: any) {
|
||||
errorMessage.value = `Error validando directorio: ${error}`;
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
validating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Usar ruta guardada
|
||||
async function useSavedPath() {
|
||||
currentPath.value = savedPath.value;
|
||||
await validateAndSave();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-selector-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.project-selector-modal {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
background-color: #2d2d30;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-header p {
|
||||
margin: 0;
|
||||
color: #cccccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 12px 16px;
|
||||
background-color: #f14c4c;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.path-display {
|
||||
background-color: #252526;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.path-label {
|
||||
font-size: 12px;
|
||||
color: #858585;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.path-value {
|
||||
font-size: 13px;
|
||||
color: #4ec9b0;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.select-btn,
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.select-btn {
|
||||
background-color: #3e3e42;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.select-btn:hover {
|
||||
background-color: #4e4e52;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.save-btn:hover:not(:disabled) {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.save-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: #252526;
|
||||
padding: 16px;
|
||||
border-left: 3px solid #0e639c;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0 0 12px 0;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.info-box li {
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-box code {
|
||||
background-color: #3e3e42;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.saved-path {
|
||||
background-color: #2d2d30;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.saved-path p {
|
||||
margin: 0 0 8px 0;
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.saved-path p:last-of-type {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.saved-value {
|
||||
color: #4ec9b0 !important;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.use-saved-btn {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: #3e3e42;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.use-saved-btn:hover {
|
||||
background-color: #0e639c;
|
||||
}
|
||||
</style>
|
||||
684
AEditor/src/components/Sidebar.vue
Normal file
684
AEditor/src/components/Sidebar.vue
Normal file
@@ -0,0 +1,684 @@
|
||||
<template>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="header-title">
|
||||
<h2>Amayo Bot Editor</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button @click="emit('toggle-database')" class="icon-btn" title="Base de Datos">
|
||||
🗄️
|
||||
</button>
|
||||
<button
|
||||
@click="emit('toggle-dev-ultra')"
|
||||
:class="['icon-btn', 'dev-ultra-btn', { active: devUltraMode }]"
|
||||
title="Modo Dev Ultra"
|
||||
>
|
||||
⚡
|
||||
</button>
|
||||
<button @click="emit('change-directory')" class="icon-btn" title="Cambiar directorio">
|
||||
📁
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-path">
|
||||
<span class="path-label">Proyecto:</span>
|
||||
<span class="path-value">{{ truncatePath(projectRoot) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="actions-section">
|
||||
<button @click="emit('new-command')" class="action-btn primary">
|
||||
➕ Nuevo Comando
|
||||
</button>
|
||||
<button @click="emit('new-event')" class="action-btn primary">
|
||||
➕ Nuevo Evento
|
||||
</button>
|
||||
<button @click="emit('toggle-env-manager')" class="action-btn env-manager">
|
||||
🔐 Variables ENV
|
||||
</button>
|
||||
<button @click="emit('refresh')" class="action-btn secondary">
|
||||
🔄 Refrescar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="files-section">
|
||||
<!-- Sección de Comandos -->
|
||||
<div class="section-group">
|
||||
<div class="section-header" @click="toggleSection('commands')">
|
||||
<span class="section-icon">{{ sectionsExpanded.commands ? '📂' : '📁' }}</span>
|
||||
<h3 class="section-title">Comandos</h3>
|
||||
<span class="section-count">{{ stats.totalCommands }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="sectionsExpanded.commands" class="section-content">
|
||||
<!-- Subsección Comandos Mensaje -->
|
||||
<div class="subsection">
|
||||
<div class="subsection-header" @click="toggleSubsection('messageCommands')">
|
||||
<span class="subsection-icon">📝</span>
|
||||
<span class="subsection-title">Comandos Mensaje</span>
|
||||
<span class="subsection-count">{{ stats.messageCommands }}</span>
|
||||
</div>
|
||||
<FileExplorer
|
||||
v-if="subsectionsExpanded.messageCommands"
|
||||
:files="props.commands.filter(c => c.commandType === 'message')"
|
||||
:selected-file="selectedFile"
|
||||
:project-root="projectRoot"
|
||||
base-path="commands"
|
||||
@select-file="(file) => emit('select-file', file)"
|
||||
@refresh="emit('refresh')"
|
||||
@notify="(msg, type) => emit('notify', msg, type)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Subsección Comandos Slash -->
|
||||
<div class="subsection">
|
||||
<div class="subsection-header" @click="toggleSubsection('slashCommands')">
|
||||
<span class="subsection-icon">⚡</span>
|
||||
<span class="subsection-title">Comandos Slash</span>
|
||||
<span class="subsection-count">{{ stats.slashCommands }}</span>
|
||||
</div>
|
||||
<FileExplorer
|
||||
v-if="subsectionsExpanded.slashCommands"
|
||||
:files="props.commands.filter(c => c.commandType === 'slash')"
|
||||
:selected-file="selectedFile"
|
||||
:project-root="projectRoot"
|
||||
base-path="commands"
|
||||
@select-file="(file) => emit('select-file', file)"
|
||||
@refresh="emit('refresh')"
|
||||
@notify="(msg, type) => emit('notify', msg, type)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección de Eventos -->
|
||||
<div class="section-group">
|
||||
<div class="section-header" @click="toggleSection('events')">
|
||||
<span class="section-icon">{{ sectionsExpanded.events ? '📂' : '📁' }}</span>
|
||||
<h3 class="section-title">Eventos</h3>
|
||||
<span class="section-count">{{ stats.totalEvents }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="sectionsExpanded.events" class="section-content">
|
||||
<!-- Subsección Eventos Estándar -->
|
||||
<div class="subsection">
|
||||
<div class="subsection-header" @click="toggleSubsection('standardEvents')">
|
||||
<span class="subsection-icon">🎯</span>
|
||||
<span class="subsection-title">Eventos Estándar</span>
|
||||
<span class="subsection-count">{{ stats.standardEvents }}</span>
|
||||
</div>
|
||||
<FileExplorer
|
||||
v-if="subsectionsExpanded.standardEvents"
|
||||
:files="props.events.filter(e => e.eventType === 'standard')"
|
||||
:selected-file="selectedFile"
|
||||
:project-root="projectRoot"
|
||||
base-path="events"
|
||||
@select-file="(file) => emit('select-file', file)"
|
||||
@refresh="emit('refresh')"
|
||||
@notify="(msg, type) => emit('notify', msg, type)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Subsección Eventos Custom -->
|
||||
<div class="subsection">
|
||||
<div class="subsection-header" @click="toggleSubsection('customEvents')">
|
||||
<span class="subsection-icon">✨</span>
|
||||
<span class="subsection-title">Eventos Custom</span>
|
||||
<span class="subsection-count">{{ stats.customEvents }}</span>
|
||||
</div>
|
||||
<FileExplorer
|
||||
v-if="subsectionsExpanded.customEvents"
|
||||
:files="props.events.filter(e => e.eventType === 'extra')"
|
||||
:selected-file="selectedFile"
|
||||
:project-root="projectRoot"
|
||||
base-path="events"
|
||||
@select-file="(file) => emit('select-file', file)"
|
||||
@refresh="emit('refresh')"
|
||||
@notify="(msg, type) => emit('notify', msg, type)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección Ultra Mode: Todos los archivos del proyecto -->
|
||||
<div v-if="devUltraMode" class="section-group ultra-mode">
|
||||
<div class="section-header" @click="toggleSection('allFiles')">
|
||||
<span class="section-icon">{{ sectionsExpanded.allFiles ? '📂' : '📁' }}</span>
|
||||
<h3 class="section-title">⚡ Archivos del Proyecto</h3>
|
||||
<span class="section-count">{{ allFiles.length }}</span>
|
||||
</div>
|
||||
|
||||
<FileExplorer
|
||||
v-if="sectionsExpanded.allFiles"
|
||||
:files="allFiles"
|
||||
:selected-file="selectedFile"
|
||||
:project-root="projectRoot"
|
||||
base-path=""
|
||||
@select-file="(file) => emit('select-file', file)"
|
||||
@refresh="emit('refresh')"
|
||||
@notify="(msg, type) => emit('notify', msg, type)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import FileExplorer from './FileExplorer.vue';
|
||||
import type { ProjectStats, FileInfo } from '../types/bot';
|
||||
|
||||
const props = defineProps<{
|
||||
stats: ProjectStats;
|
||||
commands: FileInfo[];
|
||||
events: FileInfo[];
|
||||
allFiles: FileInfo[];
|
||||
selectedFile: FileInfo | null;
|
||||
projectRoot: string;
|
||||
devUltraMode?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'new-command': [];
|
||||
'new-event': [];
|
||||
'refresh': [];
|
||||
'select-file': [file: FileInfo];
|
||||
'change-directory': [];
|
||||
'toggle-dev-ultra': [];
|
||||
'toggle-database': [];
|
||||
'toggle-env-manager': [];
|
||||
'notify': [message: string, type: 'success' | 'error' | 'info'];
|
||||
}>();
|
||||
|
||||
// Estado de expansión de secciones
|
||||
const sectionsExpanded = ref({
|
||||
commands: true,
|
||||
events: true,
|
||||
allFiles: false
|
||||
});
|
||||
|
||||
const subsectionsExpanded = ref({
|
||||
messageCommands: true,
|
||||
slashCommands: true,
|
||||
standardEvents: true,
|
||||
customEvents: true
|
||||
});
|
||||
|
||||
function toggleSection(section: 'commands' | 'events' | 'allFiles') {
|
||||
sectionsExpanded.value[section] = !sectionsExpanded.value[section];
|
||||
}
|
||||
|
||||
function toggleSubsection(subsection: keyof typeof subsectionsExpanded.value) {
|
||||
subsectionsExpanded.value[subsection] = !subsectionsExpanded.value[subsection];
|
||||
}
|
||||
|
||||
function truncatePath(path: string): string {
|
||||
if (!path) return 'No seleccionado';
|
||||
const parts = path.split(/[/\\]/);
|
||||
if (parts.length > 3) {
|
||||
return '.../' + parts.slice(-2).join('/');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
background-color: #252526;
|
||||
color: #cccccc;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 12px 16px;
|
||||
background-color: #2d2d30;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.header-title h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: #3c3c3c;
|
||||
border: 1px solid #3e3e42;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background-color: #4e4e52;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dev-ultra-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dev-ultra-btn.active {
|
||||
background-color: #0e639c;
|
||||
border-color: #1177bb;
|
||||
box-shadow: 0 0 10px rgba(14, 99, 156, 0.5);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(14, 99, 156, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(14, 99, 156, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.project-path {
|
||||
padding: 8px 16px;
|
||||
background-color: #1e1e1e;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.path-label {
|
||||
color: #858585;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.path-value {
|
||||
color: #4ec9b0;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
padding: 12px;
|
||||
background-color: #1e1e1e;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background-color: #1177bb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #3c3c3c;
|
||||
color: #cccccc;
|
||||
border: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background-color: #4e4e52;
|
||||
}
|
||||
|
||||
.action-btn.env-manager {
|
||||
background-color: #4ec9b0;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.action-btn.env-manager:hover {
|
||||
background-color: #5fd4bf;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.files-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.section-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
background-color: #3c3c3c;
|
||||
color: #cccccc;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.subsection {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.subsection-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.subsection-header:hover {
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.subsection-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #cccccc;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subsection-count {
|
||||
background-color: #3c3c3c;
|
||||
color: #858585;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #2d2d30;
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
background-color: #3e3e42;
|
||||
border-left-color: #0e639c;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-size: 10px;
|
||||
color: #858585;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
margin-left: auto;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.ultra-mode {
|
||||
border: 2px solid #0e639c;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(135deg, rgba(14, 99, 156, 0.1) 0%, rgba(14, 99, 156, 0.05) 100%);
|
||||
}
|
||||
|
||||
.ultra-mode .section-header {
|
||||
background-color: rgba(14, 99, 156, 0.2);
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
color: #858585;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e52;
|
||||
}
|
||||
.sidebar .section-title {
|
||||
background-color: #2d2d30;
|
||||
font-weight: 600;
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #4ec9b0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
height: 1px;
|
||||
background-color: #3e3e42;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 12px;
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: #3e3e42;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background-color: #4e4e52;
|
||||
}
|
||||
|
||||
.files-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 8px 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #252526;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-title h3 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #cccccc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #2a2d2e;
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
background-color: #37373d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Estilos para carpetas jerárquicas */
|
||||
.folder-group {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.folder-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #cccccc;
|
||||
background-color: #252526;
|
||||
border-left: 3px solid #007acc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.file-item.in-folder {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid #3e3e42;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.file-item.in-folder:hover {
|
||||
border-left-color: #007acc;
|
||||
}
|
||||
|
||||
/* Scrollbar personalizado */
|
||||
.sidebar::-webkit-scrollbar,
|
||||
.files-section::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track,
|
||||
.files-section::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb,
|
||||
.files-section::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover,
|
||||
.files-section::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e4e;
|
||||
}
|
||||
</style>
|
||||
189
AEditor/src/components/SkeletonLoader.vue
Normal file
189
AEditor/src/components/SkeletonLoader.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="skeleton-container">
|
||||
<div class="skeleton-sidebar">
|
||||
<div class="skeleton-header">
|
||||
<div class="skeleton-logo"></div>
|
||||
<div class="skeleton-title"></div>
|
||||
</div>
|
||||
|
||||
<div class="skeleton-stats">
|
||||
<div class="skeleton-stat" v-for="i in 4" :key="i">
|
||||
<div class="skeleton-stat-icon"></div>
|
||||
<div class="skeleton-stat-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skeleton-buttons">
|
||||
<div class="skeleton-button"></div>
|
||||
<div class="skeleton-button"></div>
|
||||
</div>
|
||||
|
||||
<div class="skeleton-list">
|
||||
<div class="skeleton-list-item" v-for="i in 8" :key="i">
|
||||
<div class="skeleton-item-icon"></div>
|
||||
<div class="skeleton-item-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skeleton-main">
|
||||
<div class="skeleton-editor-header"></div>
|
||||
<div class="skeleton-editor-content">
|
||||
<div class="skeleton-code-line" v-for="i in 20" :key="i" :style="{ width: `${60 + Math.random() * 35}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.skeleton-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #1e1e1e;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-sidebar {
|
||||
width: 280px;
|
||||
background-color: #252526;
|
||||
border-right: 1px solid #3e3e42;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.skeleton-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.skeleton-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 180px;
|
||||
height: 24px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-stats {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.skeleton-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skeleton-stat-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-stat-text {
|
||||
flex: 1;
|
||||
height: 16px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.skeleton-button {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.skeleton-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.skeleton-item-text {
|
||||
flex: 1;
|
||||
height: 14px;
|
||||
background: linear-gradient(90deg, #2d2d30 25%, #3e3e42 50%, #2d2d30 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.skeleton-editor-header {
|
||||
height: 40px;
|
||||
background: linear-gradient(90deg, #252526 25%, #2d2d30 50%, #252526 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.skeleton-editor-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.skeleton-code-line {
|
||||
height: 18px;
|
||||
margin-bottom: 10px;
|
||||
background: linear-gradient(90deg, #252526 25%, #2d2d30 50%, #252526 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
AEditor/src/main.ts
Normal file
4
AEditor/src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
564
AEditor/src/monaco-types/discord.d.ts
vendored
Normal file
564
AEditor/src/monaco-types/discord.d.ts
vendored
Normal file
@@ -0,0 +1,564 @@
|
||||
// Declaraciones básicas de Discord.js para autocompletado en Monaco Editor
|
||||
|
||||
declare module "discord.js" {
|
||||
// Client
|
||||
export class Client {
|
||||
constructor(options: ClientOptions);
|
||||
login(token: string): Promise<string>;
|
||||
destroy(): void;
|
||||
user: ClientUser | null;
|
||||
guilds: GuildManager;
|
||||
channels: ChannelManager;
|
||||
users: UserManager;
|
||||
on<K extends keyof ClientEvents>(
|
||||
event: K,
|
||||
listener: (...args: ClientEvents[K]) => void
|
||||
): this;
|
||||
once<K extends keyof ClientEvents>(
|
||||
event: K,
|
||||
listener: (...args: ClientEvents[K]) => void
|
||||
): this;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
intents: GatewayIntentBits[];
|
||||
}
|
||||
|
||||
export enum GatewayIntentBits {
|
||||
Guilds = 1 << 0,
|
||||
GuildMembers = 1 << 1,
|
||||
GuildBans = 1 << 2,
|
||||
GuildEmojisAndStickers = 1 << 3,
|
||||
GuildIntegrations = 1 << 4,
|
||||
GuildWebhooks = 1 << 5,
|
||||
GuildInvites = 1 << 6,
|
||||
GuildVoiceStates = 1 << 7,
|
||||
GuildPresences = 1 << 8,
|
||||
GuildMessages = 1 << 9,
|
||||
GuildMessageReactions = 1 << 10,
|
||||
GuildMessageTyping = 1 << 11,
|
||||
DirectMessages = 1 << 12,
|
||||
DirectMessageReactions = 1 << 13,
|
||||
DirectMessageTyping = 1 << 14,
|
||||
MessageContent = 1 << 15,
|
||||
GuildScheduledEvents = 1 << 16,
|
||||
AutoModerationConfiguration = 1 << 20,
|
||||
AutoModerationExecution = 1 << 21,
|
||||
}
|
||||
|
||||
// User & ClientUser
|
||||
export class User {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
avatar: string | null;
|
||||
bot: boolean;
|
||||
tag: string;
|
||||
displayAvatarURL(options?: ImageURLOptions): string;
|
||||
send(options: MessageCreateOptions): Promise<Message>;
|
||||
}
|
||||
|
||||
export class ClientUser extends User {
|
||||
setActivity(name: string, options?: ActivityOptions): Presence;
|
||||
setStatus(status: PresenceStatusData): Presence;
|
||||
setPresence(presence: PresenceData): Presence;
|
||||
}
|
||||
|
||||
// Message
|
||||
export class Message {
|
||||
id: string;
|
||||
content: string;
|
||||
author: User;
|
||||
channel: TextChannel | DMChannel;
|
||||
guild: Guild | null;
|
||||
member: GuildMember | null;
|
||||
createdTimestamp: number;
|
||||
createdAt: Date;
|
||||
attachments: Collection<string, Attachment>;
|
||||
embeds: Embed[];
|
||||
mentions: MessageMentions;
|
||||
reply(options: MessageReplyOptions): Promise<Message>;
|
||||
delete(): Promise<Message>;
|
||||
edit(options: MessageEditOptions): Promise<Message>;
|
||||
react(emoji: string): Promise<MessageReaction>;
|
||||
}
|
||||
|
||||
export interface MessageCreateOptions {
|
||||
content?: string;
|
||||
embeds?: EmbedBuilder[];
|
||||
components?: ActionRowBuilder<any>[];
|
||||
files?: AttachmentBuilder[];
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageReplyOptions extends MessageCreateOptions {}
|
||||
export interface MessageEditOptions extends MessageCreateOptions {}
|
||||
|
||||
// Guild
|
||||
export class Guild {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
ownerId: string;
|
||||
memberCount: number;
|
||||
channels: GuildChannelManager;
|
||||
members: GuildMemberManager;
|
||||
roles: RoleManager;
|
||||
emojis: GuildEmojiManager;
|
||||
leave(): Promise<Guild>;
|
||||
}
|
||||
|
||||
// GuildMember
|
||||
export class GuildMember {
|
||||
id: string;
|
||||
user: User;
|
||||
nickname: string | null;
|
||||
displayName: string;
|
||||
roles: GuildMemberRoleManager;
|
||||
permissions: PermissionsBitField;
|
||||
joinedTimestamp: number | null;
|
||||
kick(reason?: string): Promise<GuildMember>;
|
||||
ban(options?: BanOptions): Promise<GuildMember>;
|
||||
timeout(duration: number, reason?: string): Promise<GuildMember>;
|
||||
send(options: MessageCreateOptions): Promise<Message>;
|
||||
}
|
||||
|
||||
export interface BanOptions {
|
||||
reason?: string;
|
||||
deleteMessageDays?: number;
|
||||
}
|
||||
|
||||
// Interaction
|
||||
export class Interaction {
|
||||
id: string;
|
||||
type: InteractionType;
|
||||
user: User;
|
||||
guild: Guild | null;
|
||||
channel: TextChannel | null;
|
||||
member: GuildMember | null;
|
||||
isButton(): this is ButtonInteraction;
|
||||
isChatInputCommand(): this is ChatInputCommandInteraction;
|
||||
isStringSelectMenu(): this is StringSelectMenuInteraction;
|
||||
isModalSubmit(): this is ModalSubmitInteraction;
|
||||
}
|
||||
|
||||
export class ChatInputCommandInteraction extends Interaction {
|
||||
commandName: string;
|
||||
options: CommandInteractionOptionResolver;
|
||||
reply(options: InteractionReplyOptions): Promise<void>;
|
||||
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
|
||||
editReply(options: InteractionEditReplyOptions): Promise<Message>;
|
||||
followUp(options: InteractionReplyOptions): Promise<Message>;
|
||||
deleteReply(): Promise<void>;
|
||||
}
|
||||
|
||||
export class ButtonInteraction extends Interaction {
|
||||
customId: string;
|
||||
reply(options: InteractionReplyOptions): Promise<void>;
|
||||
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
|
||||
update(options: InteractionUpdateOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export class StringSelectMenuInteraction extends Interaction {
|
||||
customId: string;
|
||||
values: string[];
|
||||
reply(options: InteractionReplyOptions): Promise<void>;
|
||||
update(options: InteractionUpdateOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export class ModalSubmitInteraction extends Interaction {
|
||||
customId: string;
|
||||
fields: ModalSubmitFields;
|
||||
reply(options: InteractionReplyOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export interface InteractionReplyOptions {
|
||||
content?: string;
|
||||
embeds?: EmbedBuilder[];
|
||||
components?: ActionRowBuilder<any>[];
|
||||
ephemeral?: boolean;
|
||||
files?: AttachmentBuilder[];
|
||||
}
|
||||
|
||||
export interface InteractionDeferReplyOptions {
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
export interface InteractionEditReplyOptions
|
||||
extends InteractionReplyOptions {}
|
||||
export interface InteractionUpdateOptions extends InteractionReplyOptions {}
|
||||
|
||||
// Embed
|
||||
export class EmbedBuilder {
|
||||
constructor(data?: object);
|
||||
setTitle(title: string): this;
|
||||
setDescription(description: string): this;
|
||||
setColor(color: string | number): this;
|
||||
setAuthor(options: EmbedAuthorOptions): this;
|
||||
setFooter(options: EmbedFooterOptions): this;
|
||||
setImage(url: string): this;
|
||||
setThumbnail(url: string): this;
|
||||
setTimestamp(timestamp?: Date | number): this;
|
||||
setURL(url: string): this;
|
||||
addFields(...fields: EmbedField[]): this;
|
||||
}
|
||||
|
||||
export interface EmbedAuthorOptions {
|
||||
name: string;
|
||||
iconURL?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface EmbedFooterOptions {
|
||||
text: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
|
||||
export interface EmbedField {
|
||||
name: string;
|
||||
value: string;
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
export class ButtonBuilder {
|
||||
constructor(data?: object);
|
||||
setCustomId(customId: string): this;
|
||||
setLabel(label: string): this;
|
||||
setStyle(style: ButtonStyle): this;
|
||||
setEmoji(emoji: string): this;
|
||||
setURL(url: string): this;
|
||||
setDisabled(disabled: boolean): this;
|
||||
}
|
||||
|
||||
export enum ButtonStyle {
|
||||
Primary = 1,
|
||||
Secondary = 2,
|
||||
Success = 3,
|
||||
Danger = 4,
|
||||
Link = 5,
|
||||
}
|
||||
|
||||
// Select Menus
|
||||
export class StringSelectMenuBuilder {
|
||||
constructor(data?: object);
|
||||
setCustomId(customId: string): this;
|
||||
setPlaceholder(placeholder: string): this;
|
||||
addOptions(...options: StringSelectMenuOptionBuilder[]): this;
|
||||
setMinValues(minValues: number): this;
|
||||
setMaxValues(maxValues: number): this;
|
||||
setDisabled(disabled: boolean): this;
|
||||
}
|
||||
|
||||
export class StringSelectMenuOptionBuilder {
|
||||
constructor(data?: object);
|
||||
setLabel(label: string): this;
|
||||
setValue(value: string): this;
|
||||
setDescription(description: string): this;
|
||||
setEmoji(emoji: string): this;
|
||||
setDefault(isDefault: boolean): this;
|
||||
}
|
||||
|
||||
// Modal
|
||||
export class ModalBuilder {
|
||||
constructor(data?: object);
|
||||
setCustomId(customId: string): this;
|
||||
setTitle(title: string): this;
|
||||
addComponents(...components: ActionRowBuilder<TextInputBuilder>[]): this;
|
||||
}
|
||||
|
||||
export class TextInputBuilder {
|
||||
constructor(data?: object);
|
||||
setCustomId(customId: string): this;
|
||||
setLabel(label: string): this;
|
||||
setStyle(style: TextInputStyle): this;
|
||||
setPlaceholder(placeholder: string): this;
|
||||
setValue(value: string): this;
|
||||
setRequired(required: boolean): this;
|
||||
setMinLength(minLength: number): this;
|
||||
setMaxLength(maxLength: number): this;
|
||||
}
|
||||
|
||||
export enum TextInputStyle {
|
||||
Short = 1,
|
||||
Paragraph = 2,
|
||||
}
|
||||
|
||||
// Action Row
|
||||
export class ActionRowBuilder<T> {
|
||||
constructor(data?: object);
|
||||
addComponents(...components: T[]): this;
|
||||
}
|
||||
|
||||
// Attachment
|
||||
export class AttachmentBuilder {
|
||||
constructor(
|
||||
attachment: Buffer | string,
|
||||
options?: AttachmentBuilderOptions
|
||||
);
|
||||
setName(name: string): this;
|
||||
setDescription(description: string): this;
|
||||
}
|
||||
|
||||
export interface AttachmentBuilderOptions {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Permissions
|
||||
export class PermissionsBitField {
|
||||
has(permission: bigint | string, checkAdmin?: boolean): boolean;
|
||||
add(...permissions: bigint[]): this;
|
||||
remove(...permissions: bigint[]): this;
|
||||
}
|
||||
|
||||
export enum PermissionFlagsBits {
|
||||
CreateInstantInvite = 1n << 0n,
|
||||
KickMembers = 1n << 1n,
|
||||
BanMembers = 1n << 2n,
|
||||
Administrator = 1n << 3n,
|
||||
ManageChannels = 1n << 4n,
|
||||
ManageGuild = 1n << 5n,
|
||||
AddReactions = 1n << 6n,
|
||||
ViewAuditLog = 1n << 7n,
|
||||
ViewChannel = 1n << 10n,
|
||||
SendMessages = 1n << 11n,
|
||||
ManageMessages = 1n << 13n,
|
||||
EmbedLinks = 1n << 14n,
|
||||
AttachFiles = 1n << 15n,
|
||||
ReadMessageHistory = 1n << 16n,
|
||||
MentionEveryone = 1n << 17n,
|
||||
Connect = 1n << 20n,
|
||||
Speak = 1n << 21n,
|
||||
MuteMembers = 1n << 22n,
|
||||
DeafenMembers = 1n << 23n,
|
||||
MoveMembers = 1n << 24n,
|
||||
ManageRoles = 1n << 28n,
|
||||
}
|
||||
|
||||
// Channels
|
||||
export class TextChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
guild: Guild;
|
||||
send(options: MessageCreateOptions): Promise<Message>;
|
||||
bulkDelete(
|
||||
messages: number | Message[]
|
||||
): Promise<Collection<string, Message>>;
|
||||
}
|
||||
|
||||
export enum ChannelType {
|
||||
GuildText = 0,
|
||||
DM = 1,
|
||||
GuildVoice = 2,
|
||||
GuildCategory = 4,
|
||||
GuildAnnouncement = 5,
|
||||
GuildStageVoice = 13,
|
||||
GuildForum = 15,
|
||||
}
|
||||
|
||||
// Managers
|
||||
export class GuildManager {
|
||||
cache: Collection<string, Guild>;
|
||||
fetch(id: string): Promise<Guild>;
|
||||
}
|
||||
|
||||
export class ChannelManager {
|
||||
cache: Collection<string, any>;
|
||||
fetch(id: string): Promise<any>;
|
||||
}
|
||||
|
||||
export class UserManager {
|
||||
cache: Collection<string, User>;
|
||||
fetch(id: string): Promise<User>;
|
||||
}
|
||||
|
||||
export class GuildChannelManager {
|
||||
cache: Collection<string, any>;
|
||||
create(options: GuildChannelCreateOptions): Promise<any>;
|
||||
}
|
||||
|
||||
export interface GuildChannelCreateOptions {
|
||||
name: string;
|
||||
type?: ChannelType;
|
||||
topic?: string;
|
||||
parent?: string;
|
||||
}
|
||||
|
||||
export class GuildMemberManager {
|
||||
cache: Collection<string, GuildMember>;
|
||||
fetch(id: string): Promise<GuildMember>;
|
||||
ban(user: string, options?: BanOptions): Promise<void>;
|
||||
unban(user: string, reason?: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class RoleManager {
|
||||
cache: Collection<string, Role>;
|
||||
create(options: RoleCreateOptions): Promise<Role>;
|
||||
}
|
||||
|
||||
export interface RoleCreateOptions {
|
||||
name?: string;
|
||||
color?: string | number;
|
||||
permissions?: bigint[];
|
||||
hoist?: boolean;
|
||||
mentionable?: boolean;
|
||||
}
|
||||
|
||||
export class GuildMemberRoleManager {
|
||||
cache: Collection<string, Role>;
|
||||
add(role: string | Role, reason?: string): Promise<GuildMember>;
|
||||
remove(role: string | Role, reason?: string): Promise<GuildMember>;
|
||||
}
|
||||
|
||||
export class GuildEmojiManager {
|
||||
cache: Collection<string, GuildEmoji>;
|
||||
create(options: GuildEmojiCreateOptions): Promise<GuildEmoji>;
|
||||
}
|
||||
|
||||
// Collection
|
||||
export class Collection<K, V> extends Map<K, V> {
|
||||
filter(
|
||||
fn: (value: V, key: K, collection: this) => boolean
|
||||
): Collection<K, V>;
|
||||
map<T>(fn: (value: V, key: K, collection: this) => T): T[];
|
||||
find(fn: (value: V, key: K, collection: this) => boolean): V | undefined;
|
||||
first(): V | undefined;
|
||||
first(amount: number): V[];
|
||||
random(): V | undefined;
|
||||
}
|
||||
|
||||
// Events
|
||||
export interface ClientEvents {
|
||||
ready: [client: Client];
|
||||
messageCreate: [message: Message];
|
||||
messageDelete: [message: Message];
|
||||
messageUpdate: [oldMessage: Message, newMessage: Message];
|
||||
interactionCreate: [interaction: Interaction];
|
||||
guildMemberAdd: [member: GuildMember];
|
||||
guildMemberRemove: [member: GuildMember];
|
||||
guildCreate: [guild: Guild];
|
||||
guildDelete: [guild: Guild];
|
||||
}
|
||||
|
||||
// Otros tipos
|
||||
export class Role {
|
||||
id: string;
|
||||
name: string;
|
||||
color: number;
|
||||
permissions: PermissionsBitField;
|
||||
}
|
||||
|
||||
export class GuildEmoji {
|
||||
id: string;
|
||||
name: string;
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
export interface GuildEmojiCreateOptions {
|
||||
attachment: Buffer | string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class MessageMentions {
|
||||
users: Collection<string, User>;
|
||||
members: Collection<string, GuildMember>;
|
||||
channels: Collection<string, any>;
|
||||
roles: Collection<string, Role>;
|
||||
}
|
||||
|
||||
export class MessageReaction {
|
||||
emoji: GuildEmoji | string;
|
||||
count: number;
|
||||
me: boolean;
|
||||
}
|
||||
|
||||
export class Attachment {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export class Embed {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
color?: number;
|
||||
timestamp?: string;
|
||||
fields: EmbedField[];
|
||||
}
|
||||
|
||||
export class DMChannel {
|
||||
id: string;
|
||||
type: ChannelType;
|
||||
recipient: User;
|
||||
send(options: MessageCreateOptions): Promise<Message>;
|
||||
}
|
||||
|
||||
export class CommandInteractionOptionResolver {
|
||||
getString(name: string, required?: boolean): string | null;
|
||||
getInteger(name: string, required?: boolean): number | null;
|
||||
getBoolean(name: string, required?: boolean): boolean | null;
|
||||
getUser(name: string, required?: boolean): User | null;
|
||||
getMember(name: string): GuildMember | null;
|
||||
getChannel(name: string): any | null;
|
||||
getRole(name: string): Role | null;
|
||||
}
|
||||
|
||||
export class ModalSubmitFields {
|
||||
getTextInputValue(customId: string): string;
|
||||
}
|
||||
|
||||
export enum InteractionType {
|
||||
Ping = 1,
|
||||
ApplicationCommand = 2,
|
||||
MessageComponent = 3,
|
||||
ApplicationCommandAutocomplete = 4,
|
||||
ModalSubmit = 5,
|
||||
}
|
||||
|
||||
export enum ApplicationCommandOptionType {
|
||||
SubCommand = 1,
|
||||
SubCommandGroup = 2,
|
||||
String = 3,
|
||||
Integer = 4,
|
||||
Boolean = 5,
|
||||
User = 6,
|
||||
Channel = 7,
|
||||
Role = 8,
|
||||
Mentionable = 9,
|
||||
Number = 10,
|
||||
Attachment = 11,
|
||||
}
|
||||
|
||||
export interface ActivityOptions {
|
||||
type?: ActivityType;
|
||||
}
|
||||
|
||||
export enum ActivityType {
|
||||
Playing = 0,
|
||||
Streaming = 1,
|
||||
Listening = 2,
|
||||
Watching = 3,
|
||||
Competing = 5,
|
||||
}
|
||||
|
||||
export type PresenceStatusData = "online" | "idle" | "dnd" | "invisible";
|
||||
|
||||
export interface PresenceData {
|
||||
status?: PresenceStatusData;
|
||||
activities?: ActivityOptions[];
|
||||
}
|
||||
|
||||
export class Presence {
|
||||
status: PresenceStatusData;
|
||||
activities: ActivityOptions[];
|
||||
}
|
||||
|
||||
export interface ImageURLOptions {
|
||||
format?: "png" | "jpg" | "jpeg" | "webp" | "gif";
|
||||
size?: 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
|
||||
}
|
||||
}
|
||||
50
AEditor/src/types/bot.ts
Normal file
50
AEditor/src/types/bot.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Tipos que mapean la estructura del bot de Discord
|
||||
|
||||
export interface CommandMessage {
|
||||
name: string;
|
||||
type: "message";
|
||||
aliases?: string[];
|
||||
cooldown?: number;
|
||||
description?: string;
|
||||
category?: string;
|
||||
usage?: string;
|
||||
run: string; // Código de la función run como string
|
||||
}
|
||||
|
||||
export interface CommandSlash {
|
||||
name: string;
|
||||
description: string;
|
||||
type: "slash";
|
||||
options?: any[];
|
||||
cooldown?: number;
|
||||
run: string; // Código de la función run como string
|
||||
}
|
||||
|
||||
export type Command = CommandMessage | CommandSlash;
|
||||
|
||||
export interface Event {
|
||||
name: string;
|
||||
type: "standard" | "extra";
|
||||
eventName?: string; // Para eventos estándar (ready, messageCreate, etc.)
|
||||
path: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface ProjectStats {
|
||||
messageCommands: number;
|
||||
slashCommands: number;
|
||||
standardEvents: number;
|
||||
customEvents: number;
|
||||
totalCommands: number;
|
||||
totalEvents: number;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
relative_path: string;
|
||||
type: "command" | "event";
|
||||
commandType?: "message" | "slash";
|
||||
eventType?: "standard" | "extra";
|
||||
folder?: string;
|
||||
}
|
||||
7
AEditor/src/vite-env.d.ts
vendored
Normal file
7
AEditor/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
Reference in New Issue
Block a user