add chat system
This commit is contained in:
parent
073733b887
commit
1ac67ba9bd
|
|
@ -66,6 +66,12 @@ Características principales
|
||||||
- Juego de carreras (canvas): [`proyecto.open_game_race`](proyecto.py)
|
- Juego de carreras (canvas): [`proyecto.open_game_race`](proyecto.py)
|
||||||
- Abrir aplicaciones macOS con `open`: [`proyecto.launch_app`](proyecto.py)
|
- Abrir aplicaciones macOS con `open`: [`proyecto.launch_app`](proyecto.py)
|
||||||
- Monitor de red en la barra de estado: [`proyecto.network_monitor`](proyecto.py)
|
- Monitor de red en la barra de estado: [`proyecto.network_monitor`](proyecto.py)
|
||||||
|
- **Chat TCP (Multiusuario):**
|
||||||
|
- **Acceso:** Desde pestaña "Enlaces" -> Botón "Chat TCP".
|
||||||
|
- **Servidor:** Al lanzarlo, se integra en el **panel derecho de la ventana principal**. Desde ahí se visualizan logs de conexión y mensajería en tiempo real, y permite enviar mensajes como Administrador a todos los conectados. [Código servidor](chat_server.py).
|
||||||
|
- **Cliente:**
|
||||||
|
- *Desde la App:* Abre una terminal externa independiente para chatear.
|
||||||
|
- *Desde otro PC:* Solo requiere el archivo `chat_client.py` (sin dependencias extra). Ejecutar: `python chat_client.py --host IP_SERVIDOR --name TuNombre`. [Código cliente](chat_client.py).
|
||||||
|
|
||||||
Configuración y datos sensibles
|
Configuración y datos sensibles
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_PORT = 5050
|
||||||
|
ENCODING = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def _receiver_loop(rfile) -> None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = rfile.readline()
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
# Imprime mensajes entrantes en tiempo real
|
||||||
|
sys.stdout.write(line)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Cliente de chat TCP")
|
||||||
|
parser.add_argument("--host", help="IP/host del servidor (ej: 192.168.1.10)")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Puerto (por defecto {DEFAULT_PORT})")
|
||||||
|
parser.add_argument("--name", help="Tu nombre en el chat")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
host = args.host or input("IP del servidor: ").strip()
|
||||||
|
if not host:
|
||||||
|
print("Host vacío. Abortando.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
name = args.name or input("Tu nombre: ").strip()
|
||||||
|
if not name:
|
||||||
|
name = "Anon"
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect((host, args.port))
|
||||||
|
|
||||||
|
rfile = sock.makefile("r", encoding=ENCODING, newline="\n")
|
||||||
|
wfile = sock.makefile("w", encoding=ENCODING, newline="\n")
|
||||||
|
|
||||||
|
# Handshake simple con el servidor
|
||||||
|
first = rfile.readline()
|
||||||
|
if first and first.strip() == "NAME?":
|
||||||
|
wfile.write(f"NAME {name}\n")
|
||||||
|
wfile.flush()
|
||||||
|
else:
|
||||||
|
# Si el servidor no pide nombre, lo mandamos igualmente.
|
||||||
|
wfile.write(f"NAME {name}\n")
|
||||||
|
wfile.flush()
|
||||||
|
if first:
|
||||||
|
sys.stdout.write(first)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
t = threading.Thread(target=_receiver_loop, args=(rfile,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
print("Conectado. Escribe mensajes y pulsa Enter. /quit para salir.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
for line in sys.stdin:
|
||||||
|
msg = line.rstrip("\n")
|
||||||
|
wfile.write(msg + "\n")
|
||||||
|
wfile.flush()
|
||||||
|
if msg.lower() in {"/quit", "/exit"}:
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
try:
|
||||||
|
wfile.write("/quit\n")
|
||||||
|
wfile.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
DEFAULT_HOST = "0.0.0.0"
|
||||||
|
DEFAULT_PORT = 5050
|
||||||
|
ENCODING = "utf-8"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClientConn:
|
||||||
|
sock: socket.socket
|
||||||
|
addr: tuple
|
||||||
|
name: str
|
||||||
|
wfile: any
|
||||||
|
|
||||||
|
class ChatServer:
|
||||||
|
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, on_log=None):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.on_log = on_log if on_log else print
|
||||||
|
self.clients = {}
|
||||||
|
self.clients_lock = threading.Lock()
|
||||||
|
self.server_sock = None
|
||||||
|
self.running = False
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
"""Envía el mensaje al callback configurado (GUI o print)"""
|
||||||
|
self.on_log(message)
|
||||||
|
|
||||||
|
def broadcast(self, message: str, exclude: socket.socket | None = None) -> None:
|
||||||
|
"""Envía mensaje a todos los clientes conectados"""
|
||||||
|
line = message.rstrip("\n") + "\n"
|
||||||
|
with self.clients_lock:
|
||||||
|
items = list(self.clients.items())
|
||||||
|
|
||||||
|
# Log local si no está excluido el propio servidor (opcional, pero útil ver lo que se envía)
|
||||||
|
# self.log(f"BRD: {message}")
|
||||||
|
|
||||||
|
for sock, client in items:
|
||||||
|
if exclude is not None and sock is exclude:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
client.wfile.write(line)
|
||||||
|
client.wfile.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_server_message(self, text: str):
|
||||||
|
"""Mensaje desde el servidor (admin)"""
|
||||||
|
msg = f"[ADMIN] {text}"
|
||||||
|
self.log(msg)
|
||||||
|
self.broadcast(msg)
|
||||||
|
|
||||||
|
def _handle_client(self, conn: socket.socket, addr: tuple) -> None:
|
||||||
|
try:
|
||||||
|
rfile = conn.makefile("r", encoding=ENCODING, newline="\n")
|
||||||
|
wfile = conn.makefile("w", encoding=ENCODING, newline="\n")
|
||||||
|
|
||||||
|
name = None
|
||||||
|
|
||||||
|
# Protocolo simple: pedir nombre
|
||||||
|
try:
|
||||||
|
wfile.write("NAME?\n")
|
||||||
|
wfile.flush()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_name = rfile.readline()
|
||||||
|
except Exception:
|
||||||
|
raw_name = None
|
||||||
|
|
||||||
|
if not raw_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
raw_name = raw_name.strip()
|
||||||
|
# Compatibilidad si el cliente manda "NAME Pepe"
|
||||||
|
if raw_name.upper().startswith("NAME "):
|
||||||
|
raw_name = raw_name[5:].strip()
|
||||||
|
|
||||||
|
name = raw_name or f"{addr[0]}:{addr[1]}"
|
||||||
|
|
||||||
|
with self.clients_lock:
|
||||||
|
self.clients[conn] = ClientConn(sock=conn, addr=addr, name=name, wfile=wfile)
|
||||||
|
|
||||||
|
msg_join = f"* {name} se ha unido al chat *"
|
||||||
|
self.log(msg_join)
|
||||||
|
self.broadcast(msg_join)
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
line = rfile.readline()
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
msg = line.strip()
|
||||||
|
if not msg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if msg.lower() in {"/quit", "/exit"}:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Mostrar en servidor y reenviar a otros
|
||||||
|
full_msg = f"[{name}] {msg}"
|
||||||
|
self.log(full_msg)
|
||||||
|
self.broadcast(full_msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error gestionando cliente {addr}: {e}")
|
||||||
|
finally:
|
||||||
|
with self.clients_lock:
|
||||||
|
self.clients.pop(conn, None)
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if name:
|
||||||
|
msg_left = f"* {name} ha salido del chat *"
|
||||||
|
self.log(msg_left)
|
||||||
|
self.broadcast(msg_left)
|
||||||
|
|
||||||
|
def start_background(self):
|
||||||
|
"""Inicia el servidor en un hilo secundario"""
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
self.running = True
|
||||||
|
self.thread = threading.Thread(target=self._run_server_loop, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Detiene el servidor"""
|
||||||
|
self.running = False
|
||||||
|
if self.server_sock:
|
||||||
|
try:
|
||||||
|
self.server_sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.log("Servidor detenido.")
|
||||||
|
|
||||||
|
def _run_server_loop(self):
|
||||||
|
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.server_sock.bind((self.host, self.port))
|
||||||
|
self.server_sock.listen(100)
|
||||||
|
self.server_sock.settimeout(1.0) # Timeout para permitir verificar self.running
|
||||||
|
self.log(f"Servidor escuchando en {self.host}:{self.port}")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
conn, addr = self.server_sock.accept()
|
||||||
|
t = threading.Thread(target=self._handle_client, args=(conn, addr), daemon=True)
|
||||||
|
t.start()
|
||||||
|
except TimeoutError:
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error aceptando conexión: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error fatal en servidor: {e}")
|
||||||
|
finally:
|
||||||
|
self.running = False
|
||||||
|
try:
|
||||||
|
self.server_sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Modo standalone para pruebas
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
srv = ChatServer(port=5050)
|
||||||
|
srv.start_background()
|
||||||
|
print("Presiona Ctrl+C para salir.")
|
||||||
|
while True:
|
||||||
|
cmd = sys.stdin.readline()
|
||||||
|
if not cmd: break
|
||||||
|
if cmd.strip():
|
||||||
|
srv.send_server_message(cmd.strip())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
srv.stop()
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_PORT = 5050
|
||||||
|
ENCODING = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
def _receiver_loop(rfile) -> None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = rfile.readline()
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
# Imprime mensajes entrantes en tiempo real
|
||||||
|
sys.stdout.write(line)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Cliente de chat TCP")
|
||||||
|
parser.add_argument("--host", help="IP/host del servidor (ej: 192.168.1.10)")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Puerto (por defecto {DEFAULT_PORT})")
|
||||||
|
parser.add_argument("--name", help="Tu nombre en el chat")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
host = args.host or input("IP del servidor: ").strip()
|
||||||
|
if not host:
|
||||||
|
print("Host vacío. Abortando.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
name = args.name or input("Tu nombre: ").strip()
|
||||||
|
if not name:
|
||||||
|
name = "Anon"
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect((host, args.port))
|
||||||
|
|
||||||
|
rfile = sock.makefile("r", encoding=ENCODING, newline="\n")
|
||||||
|
wfile = sock.makefile("w", encoding=ENCODING, newline="\n")
|
||||||
|
|
||||||
|
# Handshake simple con el servidor
|
||||||
|
first = rfile.readline()
|
||||||
|
if first and first.strip() == "NAME?":
|
||||||
|
wfile.write(f"NAME {name}\n")
|
||||||
|
wfile.flush()
|
||||||
|
else:
|
||||||
|
# Si el servidor no pide nombre, lo mandamos igualmente.
|
||||||
|
wfile.write(f"NAME {name}\n")
|
||||||
|
wfile.flush()
|
||||||
|
if first:
|
||||||
|
sys.stdout.write(first)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
t = threading.Thread(target=_receiver_loop, args=(rfile,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
print("Conectado. Escribe mensajes y pulsa Enter. /quit para salir.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
for line in sys.stdin:
|
||||||
|
msg = line.rstrip("\n")
|
||||||
|
wfile.write(msg + "\n")
|
||||||
|
wfile.flush()
|
||||||
|
if msg.lower() in {"/quit", "/exit"}:
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
try:
|
||||||
|
wfile.write("/quit\n")
|
||||||
|
wfile.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
177
proyecto.py
177
proyecto.py
|
|
@ -1,7 +1,7 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import Menu # Importar el widget Menu
|
from tkinter import Menu, ttk, scrolledtext # Importar el widget Menu, ttk y scrolledtext
|
||||||
from tkinter import ttk # Importar el widget ttk
|
|
||||||
import threading
|
import threading
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
@ -52,6 +52,10 @@ try:
|
||||||
except Exception:
|
except Exception:
|
||||||
HAS_PYGAME = False
|
HAS_PYGAME = False
|
||||||
|
|
||||||
|
# Import chat TCP (servidor y cliente)
|
||||||
|
import chat_server
|
||||||
|
import chat_client
|
||||||
|
|
||||||
def update_time(label_widget):
|
def update_time(label_widget):
|
||||||
"""Función que actualiza la hora y el día de la semana en un label.
|
"""Función que actualiza la hora y el día de la semana en un label.
|
||||||
|
|
||||||
|
|
@ -168,6 +172,128 @@ def backup_ui():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def open_chat_window():
|
||||||
|
"""Abre ventana de configuración para lanzar servidor o cliente de chat"""
|
||||||
|
win = tk.Toplevel(root)
|
||||||
|
win.title("Chat TCP - Configuración")
|
||||||
|
win.geometry("450x400")
|
||||||
|
win.minsize(400, 350)
|
||||||
|
|
||||||
|
# Estilo personalizado para títulos
|
||||||
|
header_font = ("Helvetica", 16, "bold")
|
||||||
|
label_font = ("Helvetica", 11)
|
||||||
|
|
||||||
|
# Contenedor principal
|
||||||
|
content = tk.Frame(win, bg="#f5f5f5", padx=20, pady=20)
|
||||||
|
content.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
# Título
|
||||||
|
tk.Label(content, text="Iniciar Chat", font=header_font, bg="#f5f5f5", fg="#333").pack(pady=(0, 20))
|
||||||
|
|
||||||
|
# --- Sección ROL ---
|
||||||
|
rol_frame = tk.Frame(content, bg="white", bd=1, relief="solid", padx=15, pady=15)
|
||||||
|
rol_frame.pack(fill="x", pady=(0, 15))
|
||||||
|
|
||||||
|
tk.Label(rol_frame, text="Selecciona tu rol:", font=("Helvetica", 12, "bold"), bg="white").pack(anchor="w", pady=(0, 10))
|
||||||
|
|
||||||
|
mode_var = tk.StringVar(value="server")
|
||||||
|
|
||||||
|
# Sub-frame para radios
|
||||||
|
radios_f = tk.Frame(rol_frame, bg="white")
|
||||||
|
radios_f.pack(fill="x")
|
||||||
|
|
||||||
|
r1 = tk.Radiobutton(radios_f, text="Servidor (Anfitrión)", variable=mode_var, value="server", bg="white", font=label_font)
|
||||||
|
r1.pack(side="left", padx=(0, 20))
|
||||||
|
r2 = tk.Radiobutton(radios_f, text="Cliente (Invitado)", variable=mode_var, value="client", bg="white", font=label_font)
|
||||||
|
r2.pack(side="left")
|
||||||
|
|
||||||
|
# --- Sección DATOS ---
|
||||||
|
data_frame = tk.Frame(content, bg="white", bd=1, relief="solid", padx=15, pady=15)
|
||||||
|
data_frame.pack(fill="x", pady=(0, 20))
|
||||||
|
|
||||||
|
# Grid para formulario
|
||||||
|
tk.Label(data_frame, text="Puerto:", bg="white", font=label_font).grid(row=0, column=0, sticky="e", padx=5, pady=5)
|
||||||
|
port_var = tk.StringVar(value="5050")
|
||||||
|
tk.Entry(data_frame, textvariable=port_var, width=10, font=label_font).grid(row=0, column=1, sticky="w", padx=5, pady=5)
|
||||||
|
|
||||||
|
tk.Label(data_frame, text="IP Servidor:", bg="white", font=label_font).grid(row=1, column=0, sticky="e", padx=5, pady=5)
|
||||||
|
host_var = tk.StringVar(value="127.0.0.1")
|
||||||
|
ip_entry = tk.Entry(data_frame, textvariable=host_var, width=20, font=label_font)
|
||||||
|
ip_entry.grid(row=1, column=1, sticky="w", padx=5, pady=5)
|
||||||
|
|
||||||
|
tk.Label(data_frame, text="(Solo Cliente)", bg="white", fg="gray", font=("Arial", 9)).grid(row=1, column=2, sticky="w", padx=5)
|
||||||
|
|
||||||
|
def toggle_ip(*args):
|
||||||
|
if mode_var.get() == "server":
|
||||||
|
ip_entry.config(state="disabled", bg="#eee")
|
||||||
|
else:
|
||||||
|
ip_entry.config(state="normal", bg="white")
|
||||||
|
|
||||||
|
mode_var.trace_add("write", toggle_ip)
|
||||||
|
toggle_ip()
|
||||||
|
|
||||||
|
# --- Botón Acción ---
|
||||||
|
def run_action():
|
||||||
|
mode = mode_var.get()
|
||||||
|
try:
|
||||||
|
port = int(port_var.get())
|
||||||
|
except ValueError:
|
||||||
|
mb.showerror("Error", "El puerto debe ser numérico")
|
||||||
|
return
|
||||||
|
|
||||||
|
if mode == "server":
|
||||||
|
win.destroy()
|
||||||
|
start_server_in_ui(port)
|
||||||
|
else:
|
||||||
|
host = host_var.get()
|
||||||
|
win.destroy()
|
||||||
|
open_client_terminal(host, port)
|
||||||
|
|
||||||
|
# Botón usando ttk para asegurar renderizado nativo correcto sobre frame
|
||||||
|
# A veces tk.Button da problemas de z-order. Usamos un botón grande.
|
||||||
|
btn_frame = tk.Frame(content, bg="#f5f5f5", pady=10)
|
||||||
|
btn_frame.pack(fill="x", side="bottom")
|
||||||
|
|
||||||
|
btn = tk.Button(btn_frame, text="LANZAR APLICACIÓN", command=run_action,
|
||||||
|
bg="#007aff", fg="black", font=("Helvetica", 14, "bold"), height=2)
|
||||||
|
# En macOS 'fg' a veces no va, pero el botón debería verse.
|
||||||
|
# Usamos pack con fill x
|
||||||
|
btn.pack(fill="x")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def open_client_terminal(host, port):
|
||||||
|
"""Abre terminal externa para el cliente (interactivo)"""
|
||||||
|
if not host:
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
# Intentar detectar terminal según OS
|
||||||
|
# macOS
|
||||||
|
cmd_script = f'tell application "Terminal" to do script "cd \\"{os.getcwd()}\\" && \\"{sys.executable}\\" chat_client.py --host {host} --port {port} --name UsuarioGUI"'
|
||||||
|
try:
|
||||||
|
subprocess.run(["osascript", "-e", cmd_script])
|
||||||
|
except Exception as e:
|
||||||
|
mb.showerror("Error", f"No se pudo lanzar terminal:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def start_server_in_ui(port):
|
||||||
|
"""Inicia el servidor y conecta los logs al sidebar derecho"""
|
||||||
|
global global_chat_server
|
||||||
|
|
||||||
|
if global_chat_server and global_chat_server.running:
|
||||||
|
mb.showinfo("Servidor", "El servidor ya está corriendo. Detenlo primero si quieres reiniciar.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
global_chat_server = chat_server.ChatServer(port=port, on_log=append_chat_log)
|
||||||
|
global_chat_server.start_background()
|
||||||
|
append_chat_log(f">>> Servidor iniciado en puerto {port}")
|
||||||
|
mb.showinfo("Servidor", f"Servidor iniciado correctamente en puerto {port}.\nVer logs en panel derecho.")
|
||||||
|
except Exception as e:
|
||||||
|
mb.showerror("Error", f"Error al iniciar servidor:\n{e}")
|
||||||
|
append_chat_log(f"Error inicio: {e}")
|
||||||
|
|
||||||
|
|
||||||
def open_resource_window():
|
def open_resource_window():
|
||||||
if not HAS_MATPLOTLIB:
|
if not HAS_MATPLOTLIB:
|
||||||
mb.showwarning("Dependencia", "matplotlib no está disponible. Instálalo con pip install matplotlib")
|
mb.showwarning("Dependencia", "matplotlib no está disponible. Instálalo con pip install matplotlib")
|
||||||
|
|
@ -918,18 +1044,38 @@ sec_batch.pack(fill="x", padx=8, pady=(12,6))
|
||||||
btn_backup = ttk.Button(frame_izquierdo, text="Copias de seguridad", width=18, style="Secondary.TButton")
|
btn_backup = ttk.Button(frame_izquierdo, text="Copias de seguridad", width=18, style="Secondary.TButton")
|
||||||
btn_backup.pack(pady=6, padx=8, fill='x')
|
btn_backup.pack(pady=6, padx=8, fill='x')
|
||||||
# --- Contenido del sidebar derecho (chat y lista de alumnos) ---
|
# --- Contenido del sidebar derecho (chat y lista de alumnos) ---
|
||||||
chat_title = tk.Label(frame_derecho, text="Chat", font=("Helvetica", 14, "bold"), bg=PALETTE["sidebar"])
|
chat_title = tk.Label(frame_derecho, text="Chat Servidor", font=("Helvetica", 14, "bold"), bg=PALETTE["sidebar"])
|
||||||
chat_title.pack(pady=(8,8))
|
chat_title.pack(pady=(8,8))
|
||||||
|
|
||||||
msg_label = tk.Label(frame_derecho, text="Mensaje", bg=PALETTE["sidebar"], font=FONT_NORMAL)
|
# Área de logs del chat (solo lectura)
|
||||||
|
chat_log = scrolledtext.ScrolledText(frame_derecho, height=10, width=26, bd=0, relief="flat", state="disabled", font=("Helvetica", 9))
|
||||||
|
chat_log.pack(padx=8, pady=(0, 6), fill="x", expand=False)
|
||||||
|
|
||||||
|
msg_label = tk.Label(frame_derecho, text="Mensaje Admin", bg=PALETTE["sidebar"], font=FONT_NORMAL)
|
||||||
msg_label.pack(padx=8, anchor="w")
|
msg_label.pack(padx=8, anchor="w")
|
||||||
|
|
||||||
msg_text = tk.Text(frame_derecho, height=6, width=26, bd=0, relief="flat")
|
msg_text = tk.Text(frame_derecho, height=3, width=26, bd=0, relief="flat")
|
||||||
msg_text.pack(padx=8, pady=(6,8), fill="x")
|
msg_text.pack(padx=8, pady=(2,8), fill="x")
|
||||||
|
|
||||||
send_btn = ttk.Button(frame_derecho, text="Enviar", style="Accent.TButton")
|
send_btn = ttk.Button(frame_derecho, text="Enviar", style="Accent.TButton")
|
||||||
send_btn.pack(padx=8, pady=(0,12))
|
send_btn.pack(padx=8, pady=(0,12))
|
||||||
|
|
||||||
|
# Variable global para la instancia del servidor
|
||||||
|
global_chat_server = None
|
||||||
|
|
||||||
|
def append_chat_log(msg):
|
||||||
|
"""Callback para añadir logs al área de chat del sidebar"""
|
||||||
|
def _u():
|
||||||
|
chat_log.config(state="normal")
|
||||||
|
chat_log.insert("end", str(msg) + "\n")
|
||||||
|
chat_log.see("end")
|
||||||
|
chat_log.config(state="disabled")
|
||||||
|
# Asegurar ejecución en hilo principal
|
||||||
|
try:
|
||||||
|
chat_log.after(0, _u)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
alumnos_label = tk.Label(frame_derecho, text="Alumnos", bg=PALETTE["sidebar"], font=FONT_TITLE)
|
alumnos_label = tk.Label(frame_derecho, text="Alumnos", bg=PALETTE["sidebar"], font=FONT_TITLE)
|
||||||
alumnos_label.pack(padx=8, anchor="w")
|
alumnos_label.pack(padx=8, anchor="w")
|
||||||
|
|
||||||
|
|
@ -972,16 +1118,22 @@ btn_buscar.config(command=fetch_weather_xabia)
|
||||||
|
|
||||||
# Los controles de alarma y reproducción de música están disponibles en la pestaña "Enlaces"
|
# Los controles de alarma y reproducción de música están disponibles en la pestaña "Enlaces"
|
||||||
|
|
||||||
# Enviar mensaje (simulado)
|
# Enviar mensaje (Chat Servidor)
|
||||||
def send_message():
|
def send_message(event=None):
|
||||||
|
# Si viene de evento KeyRelease/Return, evitar salto de línea extra si es Text
|
||||||
text = msg_text.get("1.0", "end-1c").strip()
|
text = msg_text.get("1.0", "end-1c").strip()
|
||||||
if not text:
|
if not text:
|
||||||
mb.showwarning("Mensaje", "El mensaje está vacío")
|
return "break"
|
||||||
return
|
|
||||||
mb.showinfo("Mensaje", "Mensaje enviado (simulado)")
|
if global_chat_server and global_chat_server.running:
|
||||||
msg_text.delete("1.0", "end")
|
global_chat_server.send_server_message(text)
|
||||||
|
msg_text.delete("1.0", "end")
|
||||||
|
else:
|
||||||
|
mb.showwarning("Chat Servidor", "El servidor no está iniciado.\nVe a 'Enlaces > Chat TCP' e inicia el servidor.")
|
||||||
|
return "break"
|
||||||
|
|
||||||
send_btn.config(command=send_message)
|
send_btn.config(command=send_message)
|
||||||
|
msg_text.bind("<Return>", send_message)
|
||||||
|
|
||||||
# Dividir el frame central en dos partes (superior variable e inferior fija)
|
# Dividir el frame central en dos partes (superior variable e inferior fija)
|
||||||
frame_central.rowconfigure(0, weight=1) # Parte superior, tamaño variable
|
frame_central.rowconfigure(0, weight=1) # Parte superior, tamaño variable
|
||||||
|
|
@ -1155,6 +1307,7 @@ links_frame = tk.Frame(tab_enlaces)
|
||||||
links_frame.pack(fill="both", expand=True, padx=8, pady=8)
|
links_frame.pack(fill="both", expand=True, padx=8, pady=8)
|
||||||
ttk.Button(links_frame, text="Abrir Visual Studio Code", command=lambda: launch_app("/Applications/Visual Studio Code.app"), style="Accent.TButton").pack(fill="x", pady=4)
|
ttk.Button(links_frame, text="Abrir Visual Studio Code", command=lambda: launch_app("/Applications/Visual Studio Code.app"), style="Accent.TButton").pack(fill="x", pady=4)
|
||||||
ttk.Button(links_frame, text="Abrir Spotify", command=lambda: launch_app("/Applications/Spotify.app"), style="Secondary.TButton").pack(fill="x", pady=4)
|
ttk.Button(links_frame, text="Abrir Spotify", command=lambda: launch_app("/Applications/Spotify.app"), style="Secondary.TButton").pack(fill="x", pady=4)
|
||||||
|
ttk.Button(links_frame, text="Chat TCP (Servidor/Cliente)", command=open_chat_window, style="Accent.TButton").pack(fill="x", pady=4)
|
||||||
ttk.Button(links_frame, text="Mostrar recursos (matplotlib)", command=open_resource_window, style="Secondary.TButton").pack(fill="x", pady=4)
|
ttk.Button(links_frame, text="Mostrar recursos (matplotlib)", command=open_resource_window, style="Secondary.TButton").pack(fill="x", pady=4)
|
||||||
ttk.Button(links_frame, text="Reproducir música (archivo)", command=play_music_file, style="Secondary.TButton").pack(fill="x", pady=4)
|
ttk.Button(links_frame, text="Reproducir música (archivo)", command=play_music_file, style="Secondary.TButton").pack(fill="x", pady=4)
|
||||||
ttk.Button(links_frame, text="Detener música", command=stop_music, style="Secondary.TButton").pack(fill="x", pady=4)
|
ttk.Button(links_frame, text="Detener música", command=stop_music, style="Secondary.TButton").pack(fill="x", pady=4)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue