update: mejoras en chat, correo y red cliente/selector
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f916e78fe2
commit
84148e6c0e
|
|
@ -99,6 +99,7 @@ class CorreoClient:
|
||||||
|
|
||||||
def obtener_bandeja(self):
|
def obtener_bandeja(self):
|
||||||
"""Lista correos del INBOX. Devuelve lista de dicts con uid, de, asunto, fecha."""
|
"""Lista correos del INBOX. Devuelve lista de dicts con uid, de, asunto, fecha."""
|
||||||
|
self.imap.noop() # Fuerza sincronización con el servidor antes de leer
|
||||||
self.imap.select("INBOX")
|
self.imap.select("INBOX")
|
||||||
status, data = self.imap.uid("search", None, "ALL")
|
status, data = self.imap.uid("search", None, "ALL")
|
||||||
if status != "OK":
|
if status != "OK":
|
||||||
|
|
@ -108,10 +109,13 @@ class CorreoClient:
|
||||||
correos = []
|
correos = []
|
||||||
|
|
||||||
for uid in reversed(uids[-100:]): # Últimos 100, más recientes primero
|
for uid in reversed(uids[-100:]): # Últimos 100, más recientes primero
|
||||||
status, msg_data = self.imap.uid("fetch", uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
|
status, msg_data = self.imap.uid("fetch", uid, "(FLAGS BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])")
|
||||||
if status != "OK" or not msg_data[0]:
|
if status != "OK" or not msg_data[0]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
info_raw = msg_data[0][0].decode("utf-8", errors="replace")
|
||||||
|
leido = "\\Seen" in info_raw
|
||||||
|
|
||||||
raw = msg_data[0][1]
|
raw = msg_data[0][1]
|
||||||
msg = email.message_from_bytes(raw)
|
msg = email.message_from_bytes(raw)
|
||||||
|
|
||||||
|
|
@ -124,10 +128,19 @@ class CorreoClient:
|
||||||
"de": de,
|
"de": de,
|
||||||
"asunto": asunto,
|
"asunto": asunto,
|
||||||
"fecha": fecha,
|
"fecha": fecha,
|
||||||
|
"leido": leido,
|
||||||
})
|
})
|
||||||
|
|
||||||
return correos
|
return correos
|
||||||
|
|
||||||
|
def marcar_leido(self, uid):
|
||||||
|
"""Marca un correo como leído en el servidor."""
|
||||||
|
try:
|
||||||
|
self.imap.select("INBOX")
|
||||||
|
self.imap.uid("store", uid.encode(), "+FLAGS", "\\Seen")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def leer_correo(self, uid):
|
def leer_correo(self, uid):
|
||||||
"""Lee el contenido completo de un correo por UID. Devuelve texto plano."""
|
"""Lee el contenido completo de un correo por UID. Devuelve texto plano."""
|
||||||
self.imap.select("INBOX")
|
self.imap.select("INBOX")
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,23 @@ import socket
|
||||||
from logica.red.servidor import APP_FIRMA, PUERTO_BROADCAST
|
from logica.red.servidor import APP_FIRMA, PUERTO_BROADCAST
|
||||||
|
|
||||||
|
|
||||||
def descubrir_servidores(timeout=5):
|
def descubrir_servidores(timeout_sin_nuevos=10, callback=None, on_seen=None, stop_event=None):
|
||||||
"""Escucha broadcasts UDP para encontrar servidores disponibles.
|
"""Escucha broadcasts UDP para encontrar servidores disponibles.
|
||||||
|
|
||||||
Retorna una lista de tuplas (ip, puerto) de servidores encontrados.
|
- callback(ip, puerto): llamado cuando se descubre un servidor nuevo.
|
||||||
|
- on_seen(ip, puerto): llamado en cada broadcast valido (nuevos y ya conocidos).
|
||||||
|
- timeout_sin_nuevos: segundos sin nuevos servidores antes de parar (default 10).
|
||||||
|
- stop_event: threading.Event para cancelar la busqueda desde fuera.
|
||||||
|
Retorna una lista de tuplas (ip, puerto).
|
||||||
"""
|
"""
|
||||||
print(f"[DEBUG CLI] Iniciando descubrimiento UDP en puerto {PUERTO_BROADCAST} (timeout={timeout}s)")
|
import time
|
||||||
|
print(f"[DEBUG CLI] Iniciando descubrimiento UDP en puerto {PUERTO_BROADCAST} (timeout_sin_nuevos={timeout_sin_nuevos}s)")
|
||||||
servidores = []
|
servidores = []
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
if hasattr(socket, "SO_REUSEPORT"):
|
if hasattr(socket, "SO_REUSEPORT"):
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||||
sock.settimeout(timeout)
|
sock.settimeout(1)
|
||||||
try:
|
try:
|
||||||
sock.bind(("", PUERTO_BROADCAST))
|
sock.bind(("", PUERTO_BROADCAST))
|
||||||
print(f"[DEBUG CLI] Socket UDP enlazado a '':{PUERTO_BROADCAST}")
|
print(f"[DEBUG CLI] Socket UDP enlazado a '':{PUERTO_BROADCAST}")
|
||||||
|
|
@ -23,23 +28,37 @@ def descubrir_servidores(timeout=5):
|
||||||
sock.close()
|
sock.close()
|
||||||
return servidores
|
return servidores
|
||||||
|
|
||||||
|
ultimo_encontrado = time.time()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
datos, direccion = sock.recvfrom(1024)
|
if stop_event and stop_event.is_set():
|
||||||
mensaje = datos.decode("utf-8")
|
print(f"[DEBUG CLI] Busqueda cancelada externamente")
|
||||||
print(f"[DEBUG CLI] Paquete UDP recibido de {direccion}: {mensaje!r}")
|
break
|
||||||
if verificar_firma(mensaje):
|
if time.time() - ultimo_encontrado >= timeout_sin_nuevos:
|
||||||
partes = mensaje.split("|")
|
print(f"[DEBUG CLI] {timeout_sin_nuevos}s sin nuevos servidores, finalizando")
|
||||||
if len(partes) == 2:
|
break
|
||||||
puerto_servidor = int(partes[1])
|
try:
|
||||||
entrada = (direccion[0], puerto_servidor)
|
datos, direccion = sock.recvfrom(1024)
|
||||||
if entrada not in servidores:
|
mensaje = datos.decode("utf-8")
|
||||||
servidores.append(entrada)
|
print(f"[DEBUG CLI] Paquete UDP recibido de {direccion}: {mensaje!r}")
|
||||||
print(f"[DEBUG CLI] Servidor descubierto: {entrada[0]}:{entrada[1]}")
|
if verificar_firma(mensaje):
|
||||||
else:
|
partes = mensaje.split("|")
|
||||||
print(f"[DEBUG CLI] Firma no valida, ignorado")
|
if len(partes) == 2:
|
||||||
except socket.timeout:
|
puerto_servidor = int(partes[1])
|
||||||
print(f"[DEBUG CLI] Timeout alcanzado")
|
ip = direccion[0]
|
||||||
|
if on_seen:
|
||||||
|
on_seen(ip, puerto_servidor)
|
||||||
|
entrada = (ip, puerto_servidor)
|
||||||
|
if entrada not in servidores:
|
||||||
|
servidores.append(entrada)
|
||||||
|
ultimo_encontrado = time.time()
|
||||||
|
print(f"[DEBUG CLI] Servidor descubierto: {ip}:{puerto_servidor}")
|
||||||
|
if callback:
|
||||||
|
callback(ip, puerto_servidor)
|
||||||
|
else:
|
||||||
|
print(f"[DEBUG CLI] Firma no valida, ignorado")
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
finally:
|
finally:
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ def obtener_ip_local():
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
_clientes = [] # lista de (conn, addr)
|
||||||
|
_clientes_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def modo_servidor():
|
def modo_servidor():
|
||||||
servidor, puerto, contrasena_alfa, _broadcast_stop = iniciar_servidor()
|
servidor, puerto, contrasena_alfa, _broadcast_stop = iniciar_servidor()
|
||||||
clave_acceso = f"{puerto}#{contrasena_alfa}"
|
clave_acceso = f"{puerto}#{contrasena_alfa}"
|
||||||
|
|
@ -27,8 +31,22 @@ def modo_servidor():
|
||||||
print(f"[SERVIDOR] IP: {ip}")
|
print(f"[SERVIDOR] IP: {ip}")
|
||||||
print(f"[SERVIDOR] Escuchando en puerto {puerto}")
|
print(f"[SERVIDOR] Escuchando en puerto {puerto}")
|
||||||
print(f"[SERVIDOR] Clave de acceso: {clave_acceso}")
|
print(f"[SERVIDOR] Clave de acceso: {clave_acceso}")
|
||||||
print("[SERVIDOR] Esperando clientes...\n")
|
print("[SERVIDOR] Esperando clientes... (escribe para enviar a todos, Ctrl+C para salir)\n")
|
||||||
|
|
||||||
|
hilo_accept = threading.Thread(target=_aceptar_clientes, args=(servidor, clave_acceso), daemon=True)
|
||||||
|
hilo_accept.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
mensaje = input()
|
||||||
|
if mensaje:
|
||||||
|
_broadcast(f"[SERVIDOR]: {mensaje}")
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
print("\n[SERVIDOR] Cerrando...")
|
||||||
|
servidor.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _aceptar_clientes(servidor, clave_acceso):
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
conn, addr = servidor.accept()
|
conn, addr = servidor.accept()
|
||||||
|
|
@ -36,18 +54,19 @@ def modo_servidor():
|
||||||
if autenticar_cliente(datos, clave_acceso):
|
if autenticar_cliente(datos, clave_acceso):
|
||||||
conn.sendall("OK".encode("utf-8"))
|
conn.sendall("OK".encode("utf-8"))
|
||||||
print(f"[SERVIDOR] Cliente {addr[0]}:{addr[1]} autenticado")
|
print(f"[SERVIDOR] Cliente {addr[0]}:{addr[1]} autenticado")
|
||||||
hilo = threading.Thread(target=manejar_cliente, args=(conn, addr), daemon=True)
|
with _clientes_lock:
|
||||||
|
_clientes.append((conn, addr))
|
||||||
|
hilo = threading.Thread(target=_manejar_cliente, args=(conn, addr), daemon=True)
|
||||||
hilo.start()
|
hilo.start()
|
||||||
else:
|
else:
|
||||||
conn.sendall("DENIED".encode("utf-8"))
|
conn.sendall("DENIED".encode("utf-8"))
|
||||||
conn.close()
|
conn.close()
|
||||||
print(f"[SERVIDOR] Cliente {addr[0]}:{addr[1]} rechazado (clave incorrecta)")
|
print(f"[SERVIDOR] Cliente {addr[0]}:{addr[1]} rechazado (clave incorrecta)")
|
||||||
except KeyboardInterrupt:
|
except OSError:
|
||||||
print("\n[SERVIDOR] Cerrando...")
|
pass
|
||||||
servidor.close()
|
|
||||||
|
|
||||||
|
|
||||||
def manejar_cliente(conn, addr):
|
def _manejar_cliente(conn, addr):
|
||||||
etiqueta = f"{addr[0]}:{addr[1]}"
|
etiqueta = f"{addr[0]}:{addr[1]}"
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -56,14 +75,27 @@ def manejar_cliente(conn, addr):
|
||||||
break
|
break
|
||||||
mensaje = datos.decode("utf-8")
|
mensaje = datos.decode("utf-8")
|
||||||
print(f"[{etiqueta}] {mensaje}")
|
print(f"[{etiqueta}] {mensaje}")
|
||||||
conn.sendall(f"Echo: {mensaje}".encode("utf-8"))
|
_broadcast(f"{etiqueta}: {mensaje}", origen=conn)
|
||||||
except (ConnectionResetError, OSError):
|
except (ConnectionResetError, OSError):
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
with _clientes_lock:
|
||||||
|
_clientes[:] = [(c, a) for c, a in _clientes if c is not conn]
|
||||||
print(f"[SERVIDOR] Cliente {etiqueta} desconectado")
|
print(f"[SERVIDOR] Cliente {etiqueta} desconectado")
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _broadcast(mensaje, origen=None):
|
||||||
|
"""Envía un mensaje a todos los clientes excepto al origen."""
|
||||||
|
with _clientes_lock:
|
||||||
|
destinatarios = [(c, a) for c, a in _clientes if c is not origen]
|
||||||
|
for conn, addr in destinatarios:
|
||||||
|
try:
|
||||||
|
conn.sendall(mensaje.encode("utf-8"))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def buscar_servidores():
|
def buscar_servidores():
|
||||||
"""Busca servidores con reintentos. Retorna lista de servidores o None."""
|
"""Busca servidores con reintentos. Retorna lista de servidores o None."""
|
||||||
MAX_INTENTOS = 3
|
MAX_INTENTOS = 3
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class CorreosPanel(ttk.Frame):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.cliente = CorreoClient()
|
self.cliente = CorreoClient()
|
||||||
self.correos_cache = []
|
self.correos_cache = []
|
||||||
|
self._autorefresh_id = None
|
||||||
|
self._refreshing = False
|
||||||
|
|
||||||
# Frames apilados (login, bandeja, redactar)
|
# Frames apilados (login, bandeja, redactar)
|
||||||
self.frame_login = ttk.Frame(self)
|
self.frame_login = ttk.Frame(self)
|
||||||
|
|
@ -207,6 +209,7 @@ class CorreosPanel(ttk.Frame):
|
||||||
self.lbl_usuario.config(text=f" {user}")
|
self.lbl_usuario.config(text=f" {user}")
|
||||||
self._poblar_bandeja(correos)
|
self._poblar_bandeja(correos)
|
||||||
self._mostrar_frame(self.frame_bandeja)
|
self._mostrar_frame(self.frame_bandeja)
|
||||||
|
self._iniciar_autorefresh()
|
||||||
|
|
||||||
def _conexion_fallida(self, error):
|
def _conexion_fallida(self, error):
|
||||||
self.btn_conectar.config(state="normal")
|
self.btn_conectar.config(state="normal")
|
||||||
|
|
@ -253,6 +256,9 @@ class CorreosPanel(ttk.Frame):
|
||||||
self.tree.pack(side="left", fill="both", expand=True)
|
self.tree.pack(side="left", fill="both", expand=True)
|
||||||
scrollbar.pack(side="right", fill="y")
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
self.tree.tag_configure("no_leido", font=(FUENTE_FAMILIA, 9, "bold"))
|
||||||
|
self.tree.tag_configure("leido", font=(FUENTE_FAMILIA, 9))
|
||||||
|
|
||||||
self.tree.bind("<Double-1>", self._on_doble_clic_correo)
|
self.tree.bind("<Double-1>", self._on_doble_clic_correo)
|
||||||
|
|
||||||
# Área de lectura de correo
|
# Área de lectura de correo
|
||||||
|
|
@ -264,20 +270,36 @@ class CorreosPanel(ttk.Frame):
|
||||||
self.txt_lectura.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
self.txt_lectura.pack(fill="both", expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
def _poblar_bandeja(self, correos):
|
def _poblar_bandeja(self, correos):
|
||||||
"""Rellena el Treeview con la lista de correos."""
|
"""Rellena el Treeview con la lista de correos preservando la selección."""
|
||||||
self.correos_cache = correos
|
self.correos_cache = correos
|
||||||
|
sel_actual = self.tree.selection()
|
||||||
|
uid_sel = sel_actual[0] if sel_actual else None
|
||||||
|
|
||||||
self.tree.delete(*self.tree.get_children())
|
self.tree.delete(*self.tree.get_children())
|
||||||
for c in correos:
|
for c in correos:
|
||||||
|
tag = "leido" if c.get("leido", True) else "no_leido"
|
||||||
self.tree.insert("", "end", iid=c["uid"],
|
self.tree.insert("", "end", iid=c["uid"],
|
||||||
values=(c["de"], c["asunto"], c["fecha"]))
|
values=(c["de"], c["asunto"], c["fecha"]),
|
||||||
|
tags=(tag,))
|
||||||
|
|
||||||
|
if uid_sel and self.tree.exists(uid_sel):
|
||||||
|
self.tree.selection_set(uid_sel)
|
||||||
|
self.tree.see(uid_sel)
|
||||||
|
|
||||||
def _on_doble_clic_correo(self, event):
|
def _on_doble_clic_correo(self, event):
|
||||||
"""Muestra el contenido del correo seleccionado."""
|
"""Muestra el contenido del correo seleccionado y lo marca como leído."""
|
||||||
sel = self.tree.selection()
|
sel = self.tree.selection()
|
||||||
if not sel:
|
if not sel:
|
||||||
return
|
return
|
||||||
uid = sel[0]
|
uid = sel[0]
|
||||||
|
|
||||||
|
# Marcar visualmente como leído de inmediato
|
||||||
|
self.tree.item(uid, tags=("leido",))
|
||||||
|
for c in self.correos_cache:
|
||||||
|
if c["uid"] == uid:
|
||||||
|
c["leido"] = True
|
||||||
|
break
|
||||||
|
|
||||||
self.txt_lectura.config(state="normal")
|
self.txt_lectura.config(state="normal")
|
||||||
self.txt_lectura.delete("1.0", "end")
|
self.txt_lectura.delete("1.0", "end")
|
||||||
self.txt_lectura.insert("1.0", "Cargando...")
|
self.txt_lectura.insert("1.0", "Cargando...")
|
||||||
|
|
@ -286,6 +308,7 @@ class CorreosPanel(ttk.Frame):
|
||||||
def tarea():
|
def tarea():
|
||||||
try:
|
try:
|
||||||
contenido = self.cliente.leer_correo(uid)
|
contenido = self.cliente.leer_correo(uid)
|
||||||
|
self.cliente.marcar_leido(uid)
|
||||||
self.root.after(0, lambda: self._mostrar_contenido(contenido))
|
self.root.after(0, lambda: self._mostrar_contenido(contenido))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.root.after(0, lambda: self._mostrar_contenido(f"Error: {e}"))
|
self.root.after(0, lambda: self._mostrar_contenido(f"Error: {e}"))
|
||||||
|
|
@ -311,6 +334,7 @@ class CorreosPanel(ttk.Frame):
|
||||||
|
|
||||||
def _on_logout(self):
|
def _on_logout(self):
|
||||||
"""Cierra sesión y vuelve al login."""
|
"""Cierra sesión y vuelve al login."""
|
||||||
|
self._detener_autorefresh()
|
||||||
self.cliente.desconectar()
|
self.cliente.desconectar()
|
||||||
self.tree.delete(*self.tree.get_children())
|
self.tree.delete(*self.tree.get_children())
|
||||||
self.txt_lectura.config(state="normal")
|
self.txt_lectura.config(state="normal")
|
||||||
|
|
@ -409,10 +433,62 @@ class CorreosPanel(ttk.Frame):
|
||||||
self.btn_enviar.config(state="normal")
|
self.btn_enviar.config(state="normal")
|
||||||
self.lbl_estado_envio.config(text=f"Error: {error}", foreground="red")
|
self.lbl_estado_envio.config(text=f"Error: {error}", foreground="red")
|
||||||
|
|
||||||
|
# ==================================================================
|
||||||
|
# AUTO-REFRESCO
|
||||||
|
# ==================================================================
|
||||||
|
|
||||||
|
_INTERVALO_REFRESCO_MS = 1000
|
||||||
|
|
||||||
|
def _iniciar_autorefresh(self):
|
||||||
|
self._detener_autorefresh()
|
||||||
|
self._autorefresh_id = self.root.after(
|
||||||
|
self._INTERVALO_REFRESCO_MS, self._autorefresh_tick
|
||||||
|
)
|
||||||
|
|
||||||
|
def _autorefresh_tick(self):
|
||||||
|
"""Lanzado desde el hilo principal. Si ya hay refresco en curso, reintenta en el siguiente ciclo."""
|
||||||
|
if self._refreshing:
|
||||||
|
self._autorefresh_id = self.root.after(
|
||||||
|
self._INTERVALO_REFRESCO_MS, self._autorefresh_tick
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._refreshing = True
|
||||||
|
|
||||||
|
def tarea():
|
||||||
|
try:
|
||||||
|
correos = self.cliente.obtener_bandeja()
|
||||||
|
self.root.after(0, lambda: self._fin_autorefresh(correos))
|
||||||
|
except Exception:
|
||||||
|
self.root.after(0, lambda: self._fin_autorefresh(None))
|
||||||
|
|
||||||
|
threading.Thread(target=tarea, daemon=True).start()
|
||||||
|
|
||||||
|
def _fin_autorefresh(self, correos):
|
||||||
|
"""Siempre se ejecuta en el hilo principal."""
|
||||||
|
self._refreshing = False
|
||||||
|
if correos is not None:
|
||||||
|
self._poblar_bandeja(correos)
|
||||||
|
# Programar siguiente tick desde el hilo principal
|
||||||
|
self._autorefresh_id = self.root.after(
|
||||||
|
self._INTERVALO_REFRESCO_MS, self._autorefresh_tick
|
||||||
|
)
|
||||||
|
|
||||||
|
def _detener_autorefresh(self):
|
||||||
|
if self._autorefresh_id is not None:
|
||||||
|
self.root.after_cancel(self._autorefresh_id)
|
||||||
|
self._autorefresh_id = None
|
||||||
|
self._refreshing = False
|
||||||
|
|
||||||
# ==================================================================
|
# ==================================================================
|
||||||
# UTILIDADES
|
# UTILIDADES
|
||||||
# ==================================================================
|
# ==================================================================
|
||||||
|
|
||||||
|
def cerrar(self):
|
||||||
|
"""Limpieza al cerrar la aplicación: para el auto-refresco y desconecta IMAP."""
|
||||||
|
self._detener_autorefresh()
|
||||||
|
self.cliente.desconectar()
|
||||||
|
|
||||||
def _mostrar_frame(self, frame):
|
def _mostrar_frame(self, frame):
|
||||||
"""Levanta el frame indicado al frente."""
|
"""Levanta el frame indicado al frente."""
|
||||||
frame.tkraise()
|
frame.tkraise()
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class ChatBase(ttk.Frame):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.chat_history = None
|
self.chat_history = None
|
||||||
self.chat_input_entry = None
|
self.chat_input_entry = None
|
||||||
|
self._btn_enviar = None
|
||||||
self.socket = None
|
self.socket = None
|
||||||
|
|
||||||
def crear_interfaz_chat(self, parent_frame, titulo="Chat", boton_accion_texto=None, boton_accion_callback=None):
|
def crear_interfaz_chat(self, parent_frame, titulo="Chat", boton_accion_texto=None, boton_accion_callback=None):
|
||||||
|
|
@ -55,10 +56,11 @@ class ChatBase(ttk.Frame):
|
||||||
self.chat_input_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5))
|
self.chat_input_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5))
|
||||||
self.chat_input_entry.bind('<Return>', self.enviar_mensaje)
|
self.chat_input_entry.bind('<Return>', self.enviar_mensaje)
|
||||||
|
|
||||||
ttk.Button(
|
self._btn_enviar = ttk.Button(
|
||||||
frame_input, text="Enviar",
|
frame_input, text="Enviar",
|
||||||
command=self.enviar_mensaje, style='Action.TButton'
|
command=self.enviar_mensaje, style='Action.TButton'
|
||||||
).grid(row=0, column=1, sticky="e")
|
)
|
||||||
|
self._btn_enviar.grid(row=0, column=1, sticky="e")
|
||||||
|
|
||||||
def agregar_mensaje(self, remitente, texto):
|
def agregar_mensaje(self, remitente, texto):
|
||||||
"""Agrega un mensaje al historial."""
|
"""Agrega un mensaje al historial."""
|
||||||
|
|
@ -76,6 +78,13 @@ class ChatBase(ttk.Frame):
|
||||||
"""Debe ser implementado por las subclases."""
|
"""Debe ser implementado por las subclases."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def bloquear_entrada(self):
|
||||||
|
"""Deshabilita el campo de texto y el boton de enviar."""
|
||||||
|
if self.chat_input_entry:
|
||||||
|
self.chat_input_entry.config(state=tk.DISABLED)
|
||||||
|
if self._btn_enviar:
|
||||||
|
self._btn_enviar.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def cerrar_conexion(self):
|
def cerrar_conexion(self):
|
||||||
"""Cierra el socket si esta activo."""
|
"""Cierra el socket si esta activo."""
|
||||||
if self.socket:
|
if self.socket:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
import threading
|
import threading
|
||||||
from vista.chat.chat_base import ChatBase
|
from vista.chat.chat_base import ChatBase
|
||||||
from logica.red.cliente import conectar_servidor
|
from logica.red.cliente import conectar_servidor
|
||||||
|
from vista.config import *
|
||||||
|
|
||||||
PREFIJO_NOMBRE = "__NOMBRE__:"
|
PREFIJO_NOMBRE = "__NOMBRE__:"
|
||||||
|
|
||||||
|
|
@ -8,12 +11,13 @@ PREFIJO_NOMBRE = "__NOMBRE__:"
|
||||||
class ChatClientePanel(ChatBase):
|
class ChatClientePanel(ChatBase):
|
||||||
"""Vista de chat para el rol de cliente."""
|
"""Vista de chat para el rol de cliente."""
|
||||||
|
|
||||||
def __init__(self, parent, root, ip, puerto, clave, *args, **kwargs):
|
def __init__(self, parent, root, ip, puerto, clave, on_auth_error=None, *args, **kwargs):
|
||||||
super().__init__(parent, root, *args, **kwargs)
|
super().__init__(parent, root, *args, **kwargs)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.puerto = puerto
|
self.puerto = puerto
|
||||||
self.clave = clave
|
self.clave = clave
|
||||||
self.nombre = None
|
self.nombre = None
|
||||||
|
self.on_auth_error = on_auth_error
|
||||||
self.crear_interfaz_chat(
|
self.crear_interfaz_chat(
|
||||||
self, titulo="Chat - Cliente",
|
self, titulo="Chat - Cliente",
|
||||||
boton_accion_texto="Desconectar",
|
boton_accion_texto="Desconectar",
|
||||||
|
|
@ -34,7 +38,10 @@ class ChatClientePanel(ChatBase):
|
||||||
self.recibir_mensajes(extra)
|
self.recibir_mensajes(extra)
|
||||||
else:
|
else:
|
||||||
print(f"[DEBUG CLI-GUI] Conexion fallida")
|
print(f"[DEBUG CLI-GUI] Conexion fallida")
|
||||||
self.root.after(0, self.agregar_mensaje_sistema, "Error: no se pudo conectar o clave incorrecta")
|
if self.on_auth_error:
|
||||||
|
self.root.after(0, self.on_auth_error)
|
||||||
|
else:
|
||||||
|
self.root.after(0, self.agregar_mensaje_sistema, "Error: no se pudo conectar o clave incorrecta")
|
||||||
|
|
||||||
hilo = threading.Thread(target=hilo_conexion, daemon=True)
|
hilo = threading.Thread(target=hilo_conexion, daemon=True)
|
||||||
hilo.start()
|
hilo.start()
|
||||||
|
|
@ -67,14 +74,14 @@ class ChatClientePanel(ChatBase):
|
||||||
datos = self.socket.recv(4096)
|
datos = self.socket.recv(4096)
|
||||||
if not datos:
|
if not datos:
|
||||||
print("[DEBUG CLI-GUI] Datos vacios recibidos (servidor cerro)")
|
print("[DEBUG CLI-GUI] Datos vacios recibidos (servidor cerro)")
|
||||||
self.root.after(0, self.agregar_mensaje_sistema, "El servidor cerro la conexion")
|
self.root.after(0, self._servidor_desconectado)
|
||||||
break
|
break
|
||||||
mensaje = datos.decode("utf-8")
|
mensaje = datos.decode("utf-8")
|
||||||
print(f"[DEBUG CLI-GUI] Datos crudos recibidos: {datos!r}")
|
print(f"[DEBUG CLI-GUI] Datos crudos recibidos: {datos!r}")
|
||||||
self._procesar_mensaje(mensaje)
|
self._procesar_mensaje(mensaje)
|
||||||
except (ConnectionResetError, OSError) as e:
|
except (ConnectionResetError, OSError) as e:
|
||||||
print(f"[DEBUG CLI-GUI] Error en recepcion: {e}")
|
print(f"[DEBUG CLI-GUI] Error en recepcion: {e}")
|
||||||
self.root.after(0, self.agregar_mensaje_sistema, "Conexion perdida con el servidor")
|
self.root.after(0, self._servidor_desconectado)
|
||||||
|
|
||||||
def _actualizar_titulo(self):
|
def _actualizar_titulo(self):
|
||||||
if self.nombre:
|
if self.nombre:
|
||||||
|
|
@ -97,6 +104,46 @@ class ChatClientePanel(ChatBase):
|
||||||
|
|
||||||
return "break" if event else None
|
return "break" if event else None
|
||||||
|
|
||||||
|
def _servidor_desconectado(self):
|
||||||
|
"""Bloquea la entrada y muestra popup cuando el servidor cierra la conexion."""
|
||||||
|
self.cerrar_conexion()
|
||||||
|
self.bloquear_entrada()
|
||||||
|
|
||||||
|
dialogo = tk.Toplevel(self.root)
|
||||||
|
dialogo.title("Servidor desconectado")
|
||||||
|
dialogo.geometry("320x140")
|
||||||
|
dialogo.resizable(False, False)
|
||||||
|
dialogo.transient(self.root)
|
||||||
|
dialogo.grab_set()
|
||||||
|
|
||||||
|
frame = ttk.Frame(dialogo, padding=20)
|
||||||
|
frame.pack(expand=True, fill="both")
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
frame, text="El servidor se ha cerrado.",
|
||||||
|
font=FUENTE_NEGOCIOS
|
||||||
|
).pack(pady=(0, 15))
|
||||||
|
|
||||||
|
frame_btns = ttk.Frame(frame)
|
||||||
|
frame_btns.pack()
|
||||||
|
|
||||||
|
def volver():
|
||||||
|
dialogo.destroy()
|
||||||
|
self.desconectar_y_volver()
|
||||||
|
|
||||||
|
def ver_chat():
|
||||||
|
dialogo.destroy()
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
frame_btns, text="Volver al inicio",
|
||||||
|
command=volver, style='Action.TButton'
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
frame_btns, text="Ver el chat",
|
||||||
|
command=ver_chat
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
def desconectar_y_volver(self):
|
def desconectar_y_volver(self):
|
||||||
"""Desconecta del servidor y vuelve al selector."""
|
"""Desconecta del servidor y vuelve al selector."""
|
||||||
self.cerrar_conexion()
|
self.cerrar_conexion()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk, messagebox
|
||||||
import threading
|
import threading
|
||||||
from vista.config import *
|
from vista.config import *
|
||||||
from logica.red.cliente import descubrir_servidores
|
from logica.red.cliente import descubrir_servidores
|
||||||
|
|
@ -7,6 +7,14 @@ from logica.red.selector import obtener_ip_local
|
||||||
from vista.chat.chat_servidor import ChatServidorPanel
|
from vista.chat.chat_servidor import ChatServidorPanel
|
||||||
from vista.chat.chat_cliente import ChatClientePanel
|
from vista.chat.chat_cliente import ChatClientePanel
|
||||||
|
|
||||||
|
_DONUT_SIZE = 72
|
||||||
|
_DONUT_RADIUS = 26
|
||||||
|
_DONUT_GROSOR = 9
|
||||||
|
_DONUT_COLOR = COLOR_ACCION
|
||||||
|
_DONUT_TRACK = "#d0d0d0"
|
||||||
|
_DONUT_STEP = 9 # grados por frame
|
||||||
|
_DONUT_MS = 25 # ms entre frames
|
||||||
|
|
||||||
|
|
||||||
class ChatSelectorPanel(ttk.Frame):
|
class ChatSelectorPanel(ttk.Frame):
|
||||||
"""Panel inicial que permite elegir entre servidor o cliente."""
|
"""Panel inicial que permite elegir entre servidor o cliente."""
|
||||||
|
|
@ -17,14 +25,27 @@ class ChatSelectorPanel(ttk.Frame):
|
||||||
self.parent_notebook = parent_notebook
|
self.parent_notebook = parent_notebook
|
||||||
self.panel_activo = None
|
self.panel_activo = None
|
||||||
self._busqueda_cancelada = False
|
self._busqueda_cancelada = False
|
||||||
|
self._stop_event = None
|
||||||
|
self._donut_animating = False
|
||||||
|
self._donut_angle = 0
|
||||||
|
self.servidores_encontrados = []
|
||||||
|
self._ultimo_visto = {} # (ip, puerto) -> timestamp
|
||||||
|
self._frames_servidores = {} # (ip, puerto) -> ttk.Frame
|
||||||
|
self._frame_servidores = None
|
||||||
|
self._lbl_contador = None
|
||||||
|
self._lbl_buscando = None
|
||||||
|
self._canvas_donut = None
|
||||||
|
self._btn_cancelar = None
|
||||||
self.crear_selector()
|
self.crear_selector()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Pantalla principal
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def crear_selector(self):
|
def crear_selector(self):
|
||||||
"""Crea la pantalla de seleccion de rol."""
|
|
||||||
self.selector_frame = ttk.Frame(self, padding=30, style='TFrame')
|
self.selector_frame = ttk.Frame(self, padding=30, style='TFrame')
|
||||||
self.selector_frame.pack(expand=True, fill="both")
|
self.selector_frame.pack(expand=True, fill="both")
|
||||||
|
|
||||||
# Titulo
|
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
self.selector_frame, text="Chat en Red",
|
self.selector_frame, text="Chat en Red",
|
||||||
font=FUENTE_TITULO
|
font=FUENTE_TITULO
|
||||||
|
|
@ -36,7 +57,6 @@ class ChatSelectorPanel(ttk.Frame):
|
||||||
font=FUENTE_NOTA
|
font=FUENTE_NOTA
|
||||||
).pack(pady=(0, 20))
|
).pack(pady=(0, 20))
|
||||||
|
|
||||||
# Botones de rol
|
|
||||||
frame_botones = ttk.Frame(self.selector_frame, style='TFrame')
|
frame_botones = ttk.Frame(self.selector_frame, style='TFrame')
|
||||||
frame_botones.pack(pady=10)
|
frame_botones.pack(pady=10)
|
||||||
|
|
||||||
|
|
@ -50,106 +70,299 @@ class ChatSelectorPanel(ttk.Frame):
|
||||||
command=self.iniciar_busqueda_cliente, style='Action.TButton'
|
command=self.iniciar_busqueda_cliente, style='Action.TButton'
|
||||||
).pack(side="left", padx=10)
|
).pack(side="left", padx=10)
|
||||||
|
|
||||||
# Frame para resultado de busqueda (cliente)
|
|
||||||
self.frame_cliente = ttk.Frame(self.selector_frame, style='TFrame')
|
self.frame_cliente = ttk.Frame(self.selector_frame, style='TFrame')
|
||||||
self.estado_label = None
|
|
||||||
self.servidores_encontrados = []
|
# ------------------------------------------------------------------
|
||||||
|
# Servidor
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def iniciar_servidor(self):
|
def iniciar_servidor(self):
|
||||||
"""Reemplaza el selector por el panel de chat servidor."""
|
|
||||||
self.selector_frame.destroy()
|
self.selector_frame.destroy()
|
||||||
self.panel_activo = ChatServidorPanel(self, self.root)
|
self.panel_activo = ChatServidorPanel(self, self.root)
|
||||||
self.panel_activo.pack(expand=True, fill="both")
|
self.panel_activo.pack(expand=True, fill="both")
|
||||||
|
|
||||||
def iniciar_busqueda_cliente(self):
|
# ------------------------------------------------------------------
|
||||||
"""Muestra progreso y busca servidores."""
|
# Busqueda de servidores — entrada
|
||||||
self._busqueda_cancelada = False
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def iniciar_busqueda_cliente(self):
|
||||||
|
self._busqueda_cancelada = False
|
||||||
|
self.servidores_encontrados = []
|
||||||
|
self._ultimo_visto = {}
|
||||||
|
self._frames_servidores = {}
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
# Limpiar frame cliente previo
|
|
||||||
for widget in self.frame_cliente.winfo_children():
|
for widget in self.frame_cliente.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
self.frame_cliente.pack(pady=20, fill="x")
|
self.frame_cliente.pack(pady=20, fill="x")
|
||||||
|
|
||||||
self.estado_label = ttk.Label(
|
# Donut canvas
|
||||||
self.frame_cliente, text="Buscando servidores...",
|
self._canvas_donut = tk.Canvas(
|
||||||
font=FUENTE_NEGOCIOS
|
self.frame_cliente,
|
||||||
|
width=_DONUT_SIZE, height=_DONUT_SIZE,
|
||||||
|
bg=COLOR_FONDO, highlightthickness=0
|
||||||
)
|
)
|
||||||
self.estado_label.pack()
|
self._canvas_donut.pack(pady=(10, 2))
|
||||||
|
|
||||||
self.progress = ttk.Progressbar(self.frame_cliente, mode='indeterminate')
|
self._lbl_buscando = ttk.Label(
|
||||||
self.progress.pack(fill="x", padx=40, pady=5)
|
self.frame_cliente, text="Buscando...",
|
||||||
self.progress.start(15)
|
font=FUENTE_NOTA
|
||||||
|
)
|
||||||
|
self._lbl_buscando.pack()
|
||||||
|
|
||||||
ttk.Button(
|
# Contador de servidores
|
||||||
self.frame_cliente, text="Cancelar busqueda",
|
self._lbl_contador = ttk.Label(
|
||||||
|
self.frame_cliente, text="", font=FUENTE_NOTA
|
||||||
|
)
|
||||||
|
self._lbl_contador.pack(pady=(6, 0))
|
||||||
|
|
||||||
|
# Lista de servidores (se rellena en tiempo real)
|
||||||
|
self._frame_servidores = ttk.Frame(self.frame_cliente, style='TFrame')
|
||||||
|
self._frame_servidores.pack(pady=4, fill="x")
|
||||||
|
|
||||||
|
# Boton cancelar
|
||||||
|
self._btn_cancelar = ttk.Button(
|
||||||
|
self.frame_cliente, text="Cancelar búsqueda",
|
||||||
command=self.cancelar_busqueda
|
command=self.cancelar_busqueda
|
||||||
).pack(pady=5)
|
)
|
||||||
|
self._btn_cancelar.pack(pady=5)
|
||||||
|
|
||||||
hilo = threading.Thread(target=self.buscar_servidores, daemon=True)
|
self._donut_angle = 0
|
||||||
|
self._donut_animating = True
|
||||||
|
self._animar_donut()
|
||||||
|
|
||||||
|
hilo = threading.Thread(target=self._hilo_busqueda, daemon=True)
|
||||||
hilo.start()
|
hilo.start()
|
||||||
|
|
||||||
|
self.root.after(2000, self._chequear_servidores_caidos)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Animacion donut
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _animar_donut(self):
|
||||||
|
if not self._donut_animating:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if not self._canvas_donut.winfo_exists():
|
||||||
|
return
|
||||||
|
except tk.TclError:
|
||||||
|
return
|
||||||
|
cx = cy = _DONUT_SIZE // 2
|
||||||
|
r = _DONUT_RADIUS
|
||||||
|
x0, y0 = cx - r, cy - r
|
||||||
|
x1, y1 = cx + r, cy + r
|
||||||
|
|
||||||
|
self._canvas_donut.delete("all")
|
||||||
|
# Track
|
||||||
|
self._canvas_donut.create_arc(
|
||||||
|
x0, y0, x1, y1,
|
||||||
|
start=0, extent=359.9,
|
||||||
|
style=tk.ARC, outline=_DONUT_TRACK, width=_DONUT_GROSOR
|
||||||
|
)
|
||||||
|
# Arco animado
|
||||||
|
self._canvas_donut.create_arc(
|
||||||
|
x0, y0, x1, y1,
|
||||||
|
start=self._donut_angle, extent=90,
|
||||||
|
style=tk.ARC, outline=_DONUT_COLOR, width=_DONUT_GROSOR
|
||||||
|
)
|
||||||
|
self._donut_angle = (self._donut_angle + _DONUT_STEP) % 360
|
||||||
|
self.root.after(_DONUT_MS, self._animar_donut)
|
||||||
|
|
||||||
|
def _detener_donut(self):
|
||||||
|
self._donut_animating = False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Hilo de busqueda
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _hilo_busqueda(self):
|
||||||
|
descubrir_servidores(
|
||||||
|
timeout_sin_nuevos=10,
|
||||||
|
callback=lambda ip, puerto: self.root.after(
|
||||||
|
0, self._agregar_servidor, ip, puerto
|
||||||
|
),
|
||||||
|
on_seen=lambda ip, puerto: self.root.after(
|
||||||
|
0, self._actualizar_visto, ip, puerto
|
||||||
|
),
|
||||||
|
stop_event=self._stop_event,
|
||||||
|
)
|
||||||
|
if not self._busqueda_cancelada:
|
||||||
|
self.root.after(0, self._busqueda_finalizada)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Callbacks de UI
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
_TIMEOUT_SERVIDOR = 8 # segundos sin broadcast → servidor caído
|
||||||
|
|
||||||
|
def _agregar_servidor(self, ip, puerto):
|
||||||
|
"""Añade un servidor a la lista en tiempo real.
|
||||||
|
|
||||||
|
Si llega 127.0.0.1 y ya hay una IP LAN con el mismo puerto, lo ignora.
|
||||||
|
Si llega una IP LAN y ya hay 127.0.0.1 con ese puerto, reemplaza la entrada.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
LOOPBACK = "127.0.0.1"
|
||||||
|
|
||||||
|
# Buscar si ya existe una entrada con el mismo puerto
|
||||||
|
clave_existente = next(
|
||||||
|
(k for k in self.servidores_encontrados if k[1] == puerto),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
if clave_existente is not None:
|
||||||
|
ip_existente = clave_existente[0]
|
||||||
|
if ip == LOOPBACK:
|
||||||
|
return # Ya hay IP LAN → ignorar loopback
|
||||||
|
if ip_existente == LOOPBACK:
|
||||||
|
# Reemplazar loopback por IP LAN
|
||||||
|
nueva_clave = (ip, puerto)
|
||||||
|
self.servidores_encontrados.remove(clave_existente)
|
||||||
|
self.servidores_encontrados.append(nueva_clave)
|
||||||
|
frame_srv = self._frames_servidores.pop(clave_existente)
|
||||||
|
self._frames_servidores[nueva_clave] = frame_srv
|
||||||
|
self._ultimo_visto[nueva_clave] = time.time()
|
||||||
|
del self._ultimo_visto[clave_existente]
|
||||||
|
for w in frame_srv.winfo_children():
|
||||||
|
if isinstance(w, ttk.Label):
|
||||||
|
w.config(text=ip)
|
||||||
|
break
|
||||||
|
return
|
||||||
|
return # Mismo puerto, IPs distintas no-loopback → ya existe
|
||||||
|
|
||||||
|
entrada = (ip, puerto)
|
||||||
|
self.servidores_encontrados.append(entrada)
|
||||||
|
self._ultimo_visto[entrada] = time.time()
|
||||||
|
|
||||||
|
frame_srv = ttk.Frame(self._frame_servidores, style='TFrame')
|
||||||
|
frame_srv.pack(pady=2, fill="x", padx=40)
|
||||||
|
self._frames_servidores[entrada] = frame_srv
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
frame_srv, text=ip, font=FUENTE_NORMAL
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
frame_srv, text="Conectar",
|
||||||
|
command=lambda e=entrada: self.pedir_clave_entrada(e),
|
||||||
|
style='Action.TButton'
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
self._actualizar_contador()
|
||||||
|
|
||||||
|
def _actualizar_visto(self, ip, puerto):
|
||||||
|
"""Actualiza el timestamp de último broadcast recibido para un servidor."""
|
||||||
|
import time
|
||||||
|
# Buscar por puerto (independientemente de si es loopback o LAN)
|
||||||
|
clave = next(
|
||||||
|
(k for k in self.servidores_encontrados if k[1] == puerto),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if clave:
|
||||||
|
self._ultimo_visto[clave] = time.time()
|
||||||
|
|
||||||
|
def _chequear_servidores_caidos(self):
|
||||||
|
"""Elimina de la lista los servidores que llevan más de _TIMEOUT_SERVIDOR sin broadcast."""
|
||||||
|
import time
|
||||||
|
if self._busqueda_cancelada:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.frame_cliente.winfo_exists():
|
||||||
|
return
|
||||||
|
except tk.TclError:
|
||||||
|
return
|
||||||
|
|
||||||
|
ahora = time.time()
|
||||||
|
caidos = [
|
||||||
|
k for k in list(self.servidores_encontrados)
|
||||||
|
if ahora - self._ultimo_visto.get(k, ahora) > self._TIMEOUT_SERVIDOR
|
||||||
|
]
|
||||||
|
|
||||||
|
for clave in caidos:
|
||||||
|
self.servidores_encontrados.remove(clave)
|
||||||
|
del self._ultimo_visto[clave]
|
||||||
|
frame = self._frames_servidores.pop(clave, None)
|
||||||
|
if frame and frame.winfo_exists():
|
||||||
|
frame.destroy()
|
||||||
|
|
||||||
|
if caidos:
|
||||||
|
self._actualizar_contador()
|
||||||
|
|
||||||
|
self.root.after(2000, self._chequear_servidores_caidos)
|
||||||
|
|
||||||
|
def _actualizar_contador(self):
|
||||||
|
if not self._lbl_contador or not self._lbl_contador.winfo_exists():
|
||||||
|
return
|
||||||
|
n = len(self.servidores_encontrados)
|
||||||
|
self._lbl_contador.config(
|
||||||
|
text=f"{n} servidor{'es' if n != 1 else ''} encontrado{'s' if n != 1 else ''}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _busqueda_finalizada(self):
|
||||||
|
self._detener_donut()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.frame_cliente.winfo_exists():
|
||||||
|
return
|
||||||
|
except tk.TclError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ocultar donut y texto "Buscando..."
|
||||||
|
if self._canvas_donut and self._canvas_donut.winfo_exists():
|
||||||
|
self._canvas_donut.destroy()
|
||||||
|
if self._lbl_buscando and self._lbl_buscando.winfo_exists():
|
||||||
|
self._lbl_buscando.destroy()
|
||||||
|
|
||||||
|
if self._btn_cancelar and self._btn_cancelar.winfo_exists():
|
||||||
|
self._btn_cancelar.destroy()
|
||||||
|
|
||||||
|
if not self.servidores_encontrados:
|
||||||
|
if self._lbl_contador and self._lbl_contador.winfo_exists():
|
||||||
|
self._lbl_contador.config(
|
||||||
|
text="No se encontraron servidores.", foreground="red"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self._lbl_contador and self._lbl_contador.winfo_exists():
|
||||||
|
n = len(self.servidores_encontrados)
|
||||||
|
self._lbl_contador.config(
|
||||||
|
text=f"Búsqueda completada — {n} servidor{'es' if n != 1 else ''} encontrado{'s' if n != 1 else ''}",
|
||||||
|
foreground=COLOR_EXITO
|
||||||
|
)
|
||||||
|
|
||||||
|
frame_acciones = ttk.Frame(self.frame_cliente, style='TFrame')
|
||||||
|
frame_acciones.pack(pady=8)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
frame_acciones, text="Actualizar",
|
||||||
|
command=self.iniciar_busqueda_cliente, style='Action.TButton'
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
frame_acciones, text="Volver",
|
||||||
|
command=self.volver_selector
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Cancelar / volver
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def cancelar_busqueda(self):
|
def cancelar_busqueda(self):
|
||||||
"""Cancela la busqueda de servidores."""
|
|
||||||
self._busqueda_cancelada = True
|
self._busqueda_cancelada = True
|
||||||
self.progress.stop()
|
if self._stop_event:
|
||||||
|
self._stop_event.set()
|
||||||
|
self._detener_donut()
|
||||||
for widget in self.frame_cliente.winfo_children():
|
for widget in self.frame_cliente.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
self.frame_cliente.pack_forget()
|
self.frame_cliente.pack_forget()
|
||||||
|
|
||||||
def buscar_servidores(self):
|
def pedir_clave_entrada(self, srv_entrada):
|
||||||
print("[DEBUG SEL] Iniciando busqueda de servidores...")
|
ip, puerto = srv_entrada
|
||||||
servidores = descubrir_servidores(timeout=5)
|
|
||||||
print(f"[DEBUG SEL] Busqueda completada: {servidores}")
|
|
||||||
if not self._busqueda_cancelada:
|
|
||||||
self.root.after(0, self.mostrar_resultado_busqueda, servidores)
|
|
||||||
|
|
||||||
def mostrar_resultado_busqueda(self, servidores):
|
|
||||||
self.progress.stop()
|
|
||||||
for widget in self.frame_cliente.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
if not servidores:
|
|
||||||
ttk.Label(
|
|
||||||
self.frame_cliente, text="No se encontraron servidores.",
|
|
||||||
font=FUENTE_NEGOCIOS, foreground="red"
|
|
||||||
).pack()
|
|
||||||
|
|
||||||
frame_opciones = ttk.Frame(self.frame_cliente, style='TFrame')
|
|
||||||
frame_opciones.pack(pady=10)
|
|
||||||
ttk.Button(
|
|
||||||
frame_opciones, text="Reintentar",
|
|
||||||
command=self.iniciar_busqueda_cliente, style='Action.TButton'
|
|
||||||
).pack(side="left", padx=5)
|
|
||||||
ttk.Button(
|
|
||||||
frame_opciones, text="Volver",
|
|
||||||
command=self.volver_selector
|
|
||||||
).pack(side="left", padx=5)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.servidores_encontrados = servidores
|
|
||||||
|
|
||||||
ttk.Label(
|
|
||||||
self.frame_cliente,
|
|
||||||
text=f"{len(servidores)} servidor(es) encontrado(s):",
|
|
||||||
font=FUENTE_NEGOCIOS
|
|
||||||
).pack()
|
|
||||||
|
|
||||||
for i, (ip, puerto) in enumerate(servidores):
|
|
||||||
frame_srv = ttk.Frame(self.frame_cliente, style='TFrame')
|
|
||||||
frame_srv.pack(pady=2)
|
|
||||||
ttk.Label(frame_srv, text=f"{ip}", font=FUENTE_NORMAL).pack(side="left", padx=5)
|
|
||||||
ttk.Button(
|
|
||||||
frame_srv, text="Conectar",
|
|
||||||
command=lambda idx=i: self.pedir_clave(idx), style='Action.TButton'
|
|
||||||
).pack(side="left", padx=5)
|
|
||||||
|
|
||||||
def pedir_clave(self, indice):
|
|
||||||
"""Muestra un dialogo para introducir la contrasena."""
|
|
||||||
ip, puerto = self.servidores_encontrados[indice]
|
|
||||||
|
|
||||||
dialogo = tk.Toplevel(self.root)
|
dialogo = tk.Toplevel(self.root)
|
||||||
dialogo.title("Clave de acceso")
|
dialogo.title("Clave de acceso")
|
||||||
dialogo.geometry("350x120")
|
dialogo.geometry("350x160")
|
||||||
dialogo.resizable(False, False)
|
dialogo.resizable(False, False)
|
||||||
dialogo.transient(self.root)
|
dialogo.transient(self.root)
|
||||||
dialogo.grab_set()
|
dialogo.grab_set()
|
||||||
|
|
@ -174,20 +387,34 @@ class ChatSelectorPanel(ttk.Frame):
|
||||||
ttk.Button(frame, text="Conectar", command=conectar, style='Action.TButton').pack()
|
ttk.Button(frame, text="Conectar", command=conectar, style='Action.TButton').pack()
|
||||||
|
|
||||||
def conectar_a_servidor(self, ip, puerto, contrasena):
|
def conectar_a_servidor(self, ip, puerto, contrasena):
|
||||||
"""Reemplaza el selector por el panel de chat cliente."""
|
# Detener busqueda antes de destruir widgets
|
||||||
|
self._busqueda_cancelada = True
|
||||||
|
if self._stop_event:
|
||||||
|
self._stop_event.set()
|
||||||
|
self._detener_donut()
|
||||||
|
|
||||||
print(f"[DEBUG SEL] Conectando a servidor: ip={ip} puerto={puerto} clave={contrasena!r}")
|
print(f"[DEBUG SEL] Conectando a servidor: ip={ip} puerto={puerto} clave={contrasena!r}")
|
||||||
self.selector_frame.destroy()
|
self.selector_frame.destroy()
|
||||||
self.panel_activo = ChatClientePanel(self, self.root, ip, puerto, contrasena)
|
self.panel_activo = ChatClientePanel(
|
||||||
|
self, self.root, ip, puerto, contrasena,
|
||||||
|
on_auth_error=self._auth_fallida
|
||||||
|
)
|
||||||
self.panel_activo.pack(expand=True, fill="both")
|
self.panel_activo.pack(expand=True, fill="both")
|
||||||
|
|
||||||
|
def _auth_fallida(self):
|
||||||
|
"""Destruye el panel de chat y vuelve al selector con un aviso de error."""
|
||||||
|
if self.panel_activo:
|
||||||
|
self.panel_activo.destroy()
|
||||||
|
self.panel_activo = None
|
||||||
|
self.crear_selector()
|
||||||
|
messagebox.showerror("Contraseña incorrecta", "La clave introducida no es válida.\nComprueba el formato: puerto#contraseña")
|
||||||
|
|
||||||
def volver_al_selector(self):
|
def volver_al_selector(self):
|
||||||
"""Destruye el panel activo y recrea el selector."""
|
|
||||||
if self.panel_activo:
|
if self.panel_activo:
|
||||||
self.panel_activo = None
|
self.panel_activo = None
|
||||||
self.crear_selector()
|
self.crear_selector()
|
||||||
|
|
||||||
def volver_selector(self):
|
def volver_selector(self):
|
||||||
"""Limpia el frame de cliente para volver al estado inicial."""
|
|
||||||
for widget in self.frame_cliente.winfo_children():
|
for widget in self.frame_cliente.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
self.frame_cliente.pack_forget()
|
self.frame_cliente.pack_forget()
|
||||||
|
|
|
||||||
|
|
@ -171,10 +171,14 @@ class VentanaPrincipal(tk.Tk):
|
||||||
self.detener_actualizacion_reloj()
|
self.detener_actualizacion_reloj()
|
||||||
self.detener_actualizacion_clima()
|
self.detener_actualizacion_clima()
|
||||||
|
|
||||||
# Detiene el hilo de TrafficMeter y el ciclo de repintado del gráfico
|
|
||||||
if self.panel_central:
|
if self.panel_central:
|
||||||
self.panel_central.detener_actualizacion_automatica()
|
self.panel_central.detener_actualizacion_automatica()
|
||||||
|
|
||||||
|
# Desconectar correo si hay sesión activa
|
||||||
|
panel_correos = self.panel_central.modulos.get("Correos")
|
||||||
|
if panel_correos:
|
||||||
|
panel_correos.cerrar()
|
||||||
|
|
||||||
self.destroy()
|
self.destroy()
|
||||||
print("Aplicación cerrada limpiamente.")
|
print("Aplicación cerrada limpiamente.")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue