feat(vista): Refactoriza ventana principal (main)
Refactoriza la ventana principal para modularizar y mejorar la estructura. * Reestructura la ventana principal con módulos. * Integra clases modulares para cada pestaña. * Corrige errores de inicialización y dependencias. * Agrega view_scrapping.py con NavegadorPanel. * Refactoriza musicReproductor.py. * Modifica trafficMeter.py. * Modifica getWeather.py.
This commit is contained in:
parent
8904425f63
commit
9a47fe104d
|
|
@ -44,7 +44,7 @@ python -m ProyectoGlobal
|
|||
|
||||
3. ~~Programar Alarma (aviso visual y sonoro al pasar X minutos)~~
|
||||
|
||||
4. Scraping
|
||||
4. ~~Scraping~~
|
||||
|
||||
5. ~~Juego de los camellos~~ / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# Módulo: logica/T1/graficos.py
|
||||
|
||||
import psutil
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -12,28 +11,23 @@ historial_net_in = []
|
|||
historial_net_out = []
|
||||
|
||||
|
||||
def actualizar_historial_datos(net_in_kb, net_out_kb):
|
||||
def actualizar_historial_datos(net_in_kb, net_out_kb, cpu_percent, ram_percent):
|
||||
"""
|
||||
Recopila los datos actuales de CPU, RAM y añade los datos de Red
|
||||
pasados como argumento a sus historiales.
|
||||
Recopila los datos actuales de CPU, RAM y Red pasados como argumento a sus historiales.
|
||||
"""
|
||||
# 1. Obtener datos básicos (CPU y RAM)
|
||||
# interval=None asegura que se use el tiempo transcurrido desde la última llamada
|
||||
# a psutil.cpu_percent (o 0.0 si es la primera vez en este proceso)
|
||||
cpu_percent = psutil.cpu_percent(interval=None)
|
||||
ram_percent = psutil.virtual_memory().percent
|
||||
# 🎯 CORRECCIÓN: Los datos de CPU/RAM ahora vienen como argumentos, no se calculan aquí.
|
||||
|
||||
# 2. Añadir CPU y gestionar la longitud
|
||||
# 1. Añadir CPU y gestionar la longitud
|
||||
historial_cpu.append(cpu_percent)
|
||||
if len(historial_cpu) > MAX_PUNTOS:
|
||||
historial_cpu.pop(0)
|
||||
|
||||
# 3. Añadir RAM y gestionar la longitud
|
||||
# 2. Añadir RAM y gestionar la longitud
|
||||
historial_ram.append(ram_percent)
|
||||
if len(historial_ram) > MAX_PUNTOS:
|
||||
historial_ram.pop(0)
|
||||
|
||||
# 4. Añadir Red y gestionar la longitud
|
||||
# 3. Añadir Red y gestionar la longitud
|
||||
historial_net_in.append(net_in_kb)
|
||||
historial_net_out.append(net_out_kb)
|
||||
|
||||
|
|
@ -100,17 +94,6 @@ def crear_grafico_recursos(figure):
|
|||
y_limit_net = max(max_in, max_out) * 1.2 # 20% de margen
|
||||
y_limit_net = max(y_limit_net, 10) # Mínimo de 10 KB/s
|
||||
|
||||
configurar_ejes_historial(
|
||||
ax_net,
|
||||
'Tráfico de Red (KB/s) - IN: {:.1f} KB/s | OUT: {:.1f} KB/s'.format(
|
||||
historial_net_in[-1] if historial_net_in else 0,
|
||||
historial_net_out[-1] if historial_net_out else 0
|
||||
),
|
||||
'gray', [0] * MAX_PUNTOS, # Usamos un color de base para la configuración
|
||||
y_limit_net,
|
||||
[0, y_limit_net * 0.5, y_limit_net * 0.9]
|
||||
)
|
||||
|
||||
# Sobreescribir las líneas para mostrar IN y OUT
|
||||
ax_net.clear() # Limpiamos para redibujar con las dos líneas
|
||||
ax_net.set_ylim(0, y_limit_net)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ KB = 1024
|
|||
class NetIOMonitor(threading.Thread):
|
||||
"""
|
||||
Hilo que monitorea y almacena el tráfico de red (bytes por segundo)
|
||||
convirtiéndolo a Kilobytes por segundo (KB/s).
|
||||
convirtiéndolo a Kilobytes por segundo (KB/s), además de CPU y RAM.
|
||||
"""
|
||||
|
||||
def __init__(self, intervalo=1):
|
||||
|
|
@ -23,33 +23,44 @@ class NetIOMonitor(threading.Thread):
|
|||
self.lock = threading.Lock()
|
||||
self.data_in_kb = 0.0 # Tráfico de entrada en KB/s (Recibido)
|
||||
self.data_out_kb = 0.0 # Tráfico de salida en KB/s (Enviado)
|
||||
self.cpu_percent = 0.0 # Nuevo
|
||||
self.ram_percent = 0.0 # Nuevo
|
||||
|
||||
# Almacena el contador anterior para calcular la diferencia (tasa)
|
||||
self.last_counters = psutil.net_io_counters()
|
||||
# Nota: La primera lectura es solo para inicializar, se requiere una segunda para la tasa.
|
||||
|
||||
# Necesario para inicializar la medición de CPU/RAM al inicio
|
||||
psutil.cpu_percent(interval=None)
|
||||
|
||||
def run(self):
|
||||
"""Método principal del hilo."""
|
||||
while not self._stop_event.is_set():
|
||||
# Esperar el intervalo antes de la lectura para calcular la tasa
|
||||
time.sleep(self.intervalo)
|
||||
self._actualizar_datos()
|
||||
try:
|
||||
while not self._stop_event.is_set():
|
||||
# Esperar el intervalo antes de la lectura para calcular la tasa
|
||||
time.sleep(self.intervalo)
|
||||
self._actualizar_datos()
|
||||
except Exception as e:
|
||||
print(f"Error fatal en el hilo NetIOMonitor: {e}")
|
||||
self._stop_event.set()
|
||||
|
||||
def stop(self):
|
||||
"""Detiene el hilo de forma segura."""
|
||||
self._stop_event.set()
|
||||
|
||||
def _actualizar_datos(self):
|
||||
"""Calcula el tráfico de red en KB/s."""
|
||||
"""Calcula el tráfico de red en KB/s y mide CPU/RAM."""
|
||||
|
||||
current_counters = psutil.net_io_counters()
|
||||
|
||||
# Medición de CPU y RAM (usando interval=0.0 ya que el sleep garantiza el intervalo)
|
||||
current_cpu = psutil.cpu_percent(interval=0.0)
|
||||
current_ram = psutil.virtual_memory().percent
|
||||
|
||||
# Calcular la diferencia de bytes recibidos y enviados desde la última lectura
|
||||
bytes_recv_diff = current_counters.bytes_recv - self.last_counters.bytes_recv
|
||||
bytes_sent_diff = current_counters.bytes_sent - self.last_counters.bytes_sent
|
||||
|
||||
# Calcular la tasa (bytes/segundo) y convertir a KB/s
|
||||
# El tiempo transcurrido es igual a self.intervalo
|
||||
rate_in_kb_s = (bytes_recv_diff / self.intervalo) / KB
|
||||
rate_out_kb_s = (bytes_sent_diff / self.intervalo) / KB
|
||||
|
||||
|
|
@ -60,11 +71,14 @@ class NetIOMonitor(threading.Thread):
|
|||
with self.lock:
|
||||
self.data_in_kb = rate_in_kb_s
|
||||
self.data_out_kb = rate_out_kb_s
|
||||
self.cpu_percent = current_cpu
|
||||
self.ram_percent = current_ram
|
||||
|
||||
def get_io_data_kb(self):
|
||||
"""Devuelve el tráfico de entrada y salida actual en KB/s."""
|
||||
"""Devuelve el tráfico de E/S, CPU y RAM actual."""
|
||||
with self.lock:
|
||||
return self.data_in_kb, self.data_out_kb
|
||||
# Devuelve los 4 valores
|
||||
return self.data_in_kb, self.data_out_kb, self.cpu_percent, self.ram_percent
|
||||
|
||||
|
||||
# --- FUNCIÓN DE INICIO ---
|
||||
|
|
|
|||
|
|
@ -1,18 +1,126 @@
|
|||
# Módulo: logica/T2/getWeather.py
|
||||
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
|
||||
# --- CONFIGURACIÓN DE APIs ---
|
||||
# 1. API para Geocodificación por IP (Ubicación)
|
||||
IP_API_URL = "http://ip-api.com/json/"
|
||||
# 2. API de Clima (Open-Meteo, gratuita y no requiere clave para datos básicos)
|
||||
CLIMA_API_URL = "https://api.open-meteo.com/v1/forecast"
|
||||
|
||||
|
||||
def _obtener_ubicacion_por_ip():
|
||||
"""
|
||||
Obtiene la latitud, longitud y ciudad de la ubicación del usuario
|
||||
basándose en su dirección IP pública.
|
||||
"""
|
||||
try:
|
||||
# Petición a la API de IP
|
||||
response = requests.get(IP_API_URL, timeout=5)
|
||||
response.raise_for_status() # Lanza un error para códigos de estado HTTP 4xx/5xx
|
||||
data = response.json()
|
||||
|
||||
if data.get("status") == "success":
|
||||
lat = data.get("lat")
|
||||
lon = data.get("lon")
|
||||
ciudad = data.get("city", "Ubicación Desconocida")
|
||||
|
||||
print(f"🌎 [IP-API] Ubicación obtenida: {ciudad} ({lat}, {lon})")
|
||||
return lat, lon, ciudad
|
||||
else:
|
||||
print(f"❌ [IP-API] Fallo al obtener la IP. Estado: {data.get('status')}")
|
||||
return None, None, None
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ [IP-API] Error de conexión o timeout: {e}")
|
||||
return None, None, None
|
||||
|
||||
|
||||
def _obtener_clima_por_coordenadas(lat, lon):
|
||||
"""
|
||||
Obtiene la temperatura y el código de estado del tiempo usando Lat/Lon.
|
||||
Utiliza Open-Meteo.
|
||||
"""
|
||||
if lat is None or lon is None:
|
||||
return None, None # No hay coordenadas válidas
|
||||
|
||||
params = {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"current_weather": "true",
|
||||
"temperature_unit": "celsius",
|
||||
"timezone": "auto"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(CLIMA_API_URL, params=params, timeout=5)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Extracción de datos
|
||||
clima_actual = data.get("current_weather", {})
|
||||
temp = clima_actual.get("temperature")
|
||||
wmo_code = clima_actual.get("weathercode")
|
||||
|
||||
print(f"🌡️ [Open-Meteo] Datos obtenidos: Temp={temp}°C, WMO={wmo_code}")
|
||||
return temp, wmo_code
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ [Open-Meteo] Error de conexión al API de clima: {e}")
|
||||
return None, None
|
||||
except Exception as e:
|
||||
print(f"❌ [Open-Meteo] Error al procesar los datos del clima: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def _decodificar_wmo(wmo_code):
|
||||
"""
|
||||
Convierte el código WMO de Open-Meteo a una descripción amigable con emojis.
|
||||
"""
|
||||
if wmo_code is None:
|
||||
return "Desconocido ❓"
|
||||
|
||||
code = int(wmo_code)
|
||||
|
||||
# 0: Cielo despejado, 1-3: Parcialmente nublado, 45-48: Niebla
|
||||
if code <= 3:
|
||||
return "Soleado/Parcialmente Nublado 🌤️"
|
||||
# 51-67: Llovizna y Lluvia
|
||||
elif 51 <= code <= 67:
|
||||
return "Lluvia 🌧️"
|
||||
# 71-75: Nieve
|
||||
elif 71 <= code <= 75:
|
||||
return "Nieve ❄️"
|
||||
# 80-82: Chubascos (Duchas)
|
||||
elif 80 <= code <= 82:
|
||||
return "Chubascos 🌦️"
|
||||
# 95, 96, 99: Tormenta
|
||||
elif code >= 95:
|
||||
return "Tormenta ⛈️"
|
||||
# Por defecto, Nublado o Niebla
|
||||
else:
|
||||
return "Nublado ☁️"
|
||||
|
||||
|
||||
def obtener_datos_clima():
|
||||
"""
|
||||
Simula la obtención de la temperatura y el estado del tiempo local.
|
||||
Devuelve la temperatura en grados Celsius y un estado del tiempo.
|
||||
Función principal: obtiene la ubicación por IP y luego el clima para esa ubicación.
|
||||
Devuelve la temperatura en grados Celsius y el estado del tiempo.
|
||||
"""
|
||||
# Simulación de datos
|
||||
temperatura = random.randint(10, 28)
|
||||
# 1. Obtener ubicación
|
||||
lat, lon, ciudad = _obtener_ubicacion_por_ip()
|
||||
|
||||
# Simulación de estado del tiempo
|
||||
estados = ["Soleado ☀️", "Nublado ☁️", "Lluvia 🌧️", "Tormenta ⛈️"]
|
||||
estado_tiempo = random.choice(estados)
|
||||
if lat is None or lon is None:
|
||||
return "No disponible (Fallo en Ubicación)"
|
||||
|
||||
return f"{temperatura}°C ({estado_tiempo})"
|
||||
# 2. Obtener clima
|
||||
temperatura, wmo_code = _obtener_clima_por_coordenadas(lat, lon)
|
||||
|
||||
if temperatura is None or wmo_code is None:
|
||||
return f"Clima no disponible en {ciudad}"
|
||||
|
||||
# 3. Decodificar y formatear
|
||||
estado_tiempo = _decodificar_wmo(wmo_code)
|
||||
|
||||
return f"{temperatura}°C ({estado_tiempo}) en {ciudad}"
|
||||
|
|
@ -1,77 +1,97 @@
|
|||
# Módulo: logica/T2/musicReproductor.py
|
||||
|
||||
import vlc
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# --- BLOQUE DE CÓDIGO OPCIONAL PARA SOLUCIÓN DE ERRORES DE RUTA DE VLC ---
|
||||
# Si al ejecutar el programa obtienes un error de "ImportError: DLL load failed"
|
||||
# o similar con 'vlc', DESCOMENTA el siguiente bloque y AJUSTA la ruta de vlc_path.
|
||||
# Esto ayuda a que Python encuentre las librerías principales de VLC.
|
||||
#
|
||||
# if sys.platform.startswith('win'):
|
||||
# # RUTA DE EJEMPLO PARA WINDOWS (AJUSTA según tu instalación)
|
||||
# vlc_path = r"C:\Program Files\VideoLAN\VLC"
|
||||
# if vlc_path not in os.environ.get('PATH', ''):
|
||||
# os.environ['PATH'] += os.pathsep + vlc_path
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class MusicReproductor:
|
||||
"""
|
||||
Clase para gestionar la reproducción de audio utilizando la librería python-vlc.
|
||||
Se asegura de que el objeto 'player' de VLC sea liberado y recreado correctamente
|
||||
para poder manejar múltiples eventos de alarma o cambiar de stream de radio sin fallos.
|
||||
Gestiona la reproducción de streams de radio usando la librería python-vlc.
|
||||
"""
|
||||
|
||||
def __init__(self, initial_volume=50):
|
||||
# 1. Crear la instancia de VLC, que debe ser única por aplicación.
|
||||
def __init__(self, initial_volume=50.0):
|
||||
"""Inicializa la instancia de VLC y el reproductor."""
|
||||
|
||||
# Instancia de VLC y objeto Reproductor
|
||||
self.instance = vlc.Instance()
|
||||
|
||||
# 2. Creamos el player inicial.
|
||||
self.player = self.instance.media_player_new()
|
||||
self.current_media = None
|
||||
self.is_playing = False
|
||||
|
||||
self.volumen = initial_volume
|
||||
# Configurar volumen inicial
|
||||
self.ajustar_volumen(initial_volume)
|
||||
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.volumen}")
|
||||
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.player.audio_get_volume()}")
|
||||
|
||||
def ajustar_volumen(self, valor):
|
||||
"""Ajusta el volumen del reproductor."""
|
||||
self.volumen = int(valor)
|
||||
if self.player:
|
||||
self.player.audio_set_volume(self.volumen)
|
||||
|
||||
def cargar_y_reproducir(self, url):
|
||||
def ajustar_volumen(self, valor_porcentual):
|
||||
"""
|
||||
Carga el archivo o stream y lo reproduce.
|
||||
|
||||
**Corrección:** Recrea self.player si fue liberado (release) por el método detener().
|
||||
Ajusta el volumen del reproductor (0 a 100).
|
||||
"""
|
||||
print(f"🔄 [VLC] Intentando cargar y reproducir: {url}")
|
||||
volumen_int = int(max(0, min(100, valor_porcentual)))
|
||||
self.player.audio_set_volume(volumen_int)
|
||||
# No imprimimos el volumen aquí para evitar saturar la consola con cada movimiento del Scale
|
||||
|
||||
# 1. Recrear el reproductor si fue liberado.
|
||||
if not self.player:
|
||||
self.player = self.instance.media_player_new()
|
||||
self.ajustar_volumen(self.volumen) # Restaurar volumen
|
||||
def cargar_y_reproducir(self, url_stream):
|
||||
"""
|
||||
Carga una nueva URL de stream y comienza la reproducción.
|
||||
"""
|
||||
if not url_stream:
|
||||
print("❌ [VLC] URL del stream vacía.")
|
||||
return
|
||||
|
||||
# 2. Detener la reproducción actual de forma segura antes de cargar una nueva media.
|
||||
# Esto previene el AttributeError, ya que self.player ahora está garantizado que no es None.
|
||||
if self.player:
|
||||
self.player.stop()
|
||||
print(f"🔄 [VLC] Intentando cargar y reproducir: {url_stream}")
|
||||
|
||||
# 3. Cargar y reproducir la nueva media.
|
||||
media = self.instance.media_new(url)
|
||||
self.player.set_media(media)
|
||||
self.player.stop()
|
||||
|
||||
if self.player.play() == 0:
|
||||
print("✅ [VLC] Reproducción iniciada.")
|
||||
self.current_media = self.instance.media_new(url_stream)
|
||||
self.player.set_media(self.current_media)
|
||||
|
||||
self.player.play()
|
||||
self.is_playing = True
|
||||
print("✅ [VLC] Reproducción iniciada.")
|
||||
|
||||
def reproducir(self):
|
||||
"""
|
||||
Reanuda la reproducción si está pausada.
|
||||
"""
|
||||
if self.player.get_state() == vlc.State.Paused:
|
||||
self.player.play()
|
||||
self.is_playing = True
|
||||
print("▶️ [VLC] Reproducción reanudada.")
|
||||
else:
|
||||
print("❌ [VLC] Error al intentar iniciar la reproducción.")
|
||||
print("ℹ️ [VLC] Ya está reproduciéndose o esperando un stream.")
|
||||
|
||||
def pausar(self):
|
||||
"""
|
||||
Pausa la reproducción.
|
||||
"""
|
||||
if self.player.get_state() == vlc.State.Playing:
|
||||
self.player.pause()
|
||||
self.is_playing = False
|
||||
print("⏸️ [VLC] Reproducción pausada.")
|
||||
else:
|
||||
print("ℹ️ [VLC] No se puede pausar, el reproductor no está en estado de reproducción.")
|
||||
|
||||
def detener(self):
|
||||
"""
|
||||
Detiene la reproducción, libera los recursos del player y lo establece a None.
|
||||
Esto es crucial para que el sistema de audio no se quede bloqueado por VLC.
|
||||
Detiene la reproducción y libera los recursos. Crucial al cerrar la aplicación.
|
||||
"""
|
||||
if self.player:
|
||||
self.player.stop()
|
||||
# 🎯 Solo liberamos el reproductor. No eliminamos self.instance.
|
||||
self.player.release()
|
||||
self.player = None # <--- Obliga a recrear el player en la próxima llamada a cargar_y_reproducir
|
||||
print("⏹️ [VLC] Reproductor detenido y recursos liberados.")
|
||||
|
||||
def pausar(self):
|
||||
"""Pausa la reproducción."""
|
||||
if self.player and self.player.is_playing():
|
||||
self.player.pause()
|
||||
|
||||
def reproducir(self):
|
||||
"""Reanuda la reproducción."""
|
||||
if self.player:
|
||||
self.player.play()
|
||||
self.player = None # Esto asegura que el player se recree si es necesario
|
||||
print("⏹️ [VLC] Reproductor detenido y recursos liberados.")
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
[
|
||||
{
|
||||
"nombre": "Esencia FM",
|
||||
"url_stream": "https://stream.serviciospararadios.es/listen/esencia_fm/esenciafm.mp3",
|
||||
"pais": "ES",
|
||||
"genero": null
|
||||
},
|
||||
{
|
||||
"nombre": "Bikini FM",
|
||||
"url_stream": "https://stream.emisorasmusicales.net/listen/bikini_fm/bikinifm-vlc.mp3",
|
||||
"pais": "ES",
|
||||
"genero": "La radio remember"
|
||||
},
|
||||
{
|
||||
"nombre": "Activa FM",
|
||||
"url_stream": "https://stream.serviciospararadios.es/listen/activa_fm/activafm-tunein.mp3",
|
||||
"pais": "ES",
|
||||
"genero": null
|
||||
},
|
||||
{
|
||||
"nombre": "Cope Denia",
|
||||
"url_stream": "https://denia-copesedes-rrcast.flumotion.com/copesedes/denia-low.mp3",
|
||||
"pais": "ES",
|
||||
"genero": "Noticias"
|
||||
},
|
||||
{
|
||||
"nombre": "KPOO",
|
||||
"url_stream": "http://amber.streamguys.com:5220/xstream",
|
||||
"pais": null,
|
||||
"genero": null
|
||||
},
|
||||
{
|
||||
"nombre": "Cope Valencia",
|
||||
"url_stream": "https://valencia-copesedes-rrcast.flumotion.com/copesedes/valencia-low.mp3",
|
||||
"pais": "ES",
|
||||
"genero": "Noticias"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'ola') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=ola&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: Ola
|
||||
FECHA/HORA: 2025-12-05 16:12:56
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 1529 chars):
|
||||
----------------------------------------------------------
|
||||
En fluidodinámica, las olas son ondas que se desplazan a través de la superficie de mares, océanos, ríos, lagos, canales y otros cuerpos de agua. Son generadas por el viento, que al soplar crea fuerzas de presión y fricción que perturban el equilibrio de la superficie de los océanos. El viento transfiere parte de su energía a las olas, ejerciendo una fuerza sobre la superficie del agua resultante de las diferencias de presión causadas por las fluctuaciones de la velocidad del viento cerca de la interfase entre aire y mar. La superficie alterada se restablece por acción de la gravedad. La interacción cíclica entre la fuerza de presión ejercida por el viento y la fuerza de gravedad hace que las olas se propaguen, y se alejen progresivamente de su zona de generación.[1]
|
||||
Explicación física
|
||||
Las olas del mar son ondas que se propagan por la superficie entre dos medios materiales. En este caso, se trata del límite entre la atmósfera y el océano. Cuando una ola se propaga en aguas profundas (a una profundidad mayor a 1/20 de su longitud de onda), las moléculas de agua regresan casi al mismo sitio donde se encontraban inicialmente. Se trata de un vaivén con una componente vertical, de arriba abajo, y otra longitudinal, igual a la dirección de propagación de la onda.
|
||||
Hay que distinguir dos movimientos. El primero es la oscilación del medio movido por la onda, que en este caso, como hemos visto, es un movimiento circular. El segundo es la propagación de la onda, que se produce porque l
|
||||
[... Contenido truncado ...]
|
||||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'ola') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=ola&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: Ola
|
||||
FECHA/HORA: 2025-12-05 16:16:28
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 1529 chars):
|
||||
----------------------------------------------------------
|
||||
En fluidodinámica, las olas son ondas que se desplazan a través de la superficie de mares, océanos, ríos, lagos, canales y otros cuerpos de agua. Son generadas por el viento, que al soplar crea fuerzas de presión y fricción que perturban el equilibrio de la superficie de los océanos. El viento transfiere parte de su energía a las olas, ejerciendo una fuerza sobre la superficie del agua resultante de las diferencias de presión causadas por las fluctuaciones de la velocidad del viento cerca de la interfase entre aire y mar. La superficie alterada se restablece por acción de la gravedad. La interacción cíclica entre la fuerza de presión ejercida por el viento y la fuerza de gravedad hace que las olas se propaguen, y se alejen progresivamente de su zona de generación.[1]
|
||||
Explicación física
|
||||
Las olas del mar son ondas que se propagan por la superficie entre dos medios materiales. En este caso, se trata del límite entre la atmósfera y el océano. Cuando una ola se propaga en aguas profundas (a una profundidad mayor a 1/20 de su longitud de onda), las moléculas de agua regresan casi al mismo sitio donde se encontraban inicialmente. Se trata de un vaivén con una componente vertical, de arriba abajo, y otra longitudinal, igual a la dirección de propagación de la onda.
|
||||
Hay que distinguir dos movimientos. El primero es la oscilación del medio movido por la onda, que en este caso, como hemos visto, es un movimiento circular. El segundo es la propagación de la onda, que se produce porque l
|
||||
[... Contenido truncado ...]
|
||||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'google') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=google&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: Google
|
||||
FECHA/HORA: 2025-12-05 16:20:06
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 1529 chars):
|
||||
----------------------------------------------------------
|
||||
Google LLC (pronunciado /ˈɡuːɡəl/ (escucharⓘ), más conocida como Google) es una empresa de tecnología multinacional con sede en California, Estados Unidos, que se centra en inteligencia artificial, publicidad en línea, tecnología de motores de búsqueda, computación en la nube, software, computación cuántica, comercio electrónico y electrónica de consumo.[1] Es una de las marcas más valiosas del mundo debido a su dominio del mercado, recopilación de datos y ventajas tecnológicas en el campo de la inteligencia artificial. Es considerada una de las cinco grandes compañías tecnológicas junto con Apple, Amazon, Microsoft y Meta Platforms.[2][3][4]
|
||||
Su producto principal es el motor de búsqueda de contenido en internet del mismo nombre, aunque ofrece también otros productos y servicios, como su servicio de almacenamiento en la nube Google Drive, el correo electrónico Gmail, sus servicios de mapas Google Maps, Google Street View y Google Earth, el sitio web de vídeos YouTube, entre otros.
|
||||
Por otra parte, lidera el desarrollo del sistema operativo y servicios de operación de aplicaciones basado en Linux: Android, orientado a teléfonos inteligentes, tabletas, televisores, automóviles y gafas de realidad aumentada, las Google Glass. Su eslogan es «Do the Right Thing» («Haz lo correcto»).[5]
|
||||
Con miles de servidores y centros de datos presentes en todo el mundo, Google es capaz de procesar más de 1000 millones de peticiones de búsqueda diarias y su motor de búsqueda es el sitio web m
|
||||
[... Contenido truncado ...]
|
||||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'ff') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=ff&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: FF
|
||||
FECHA/HORA: 2025-12-05 17:09:02
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 461 chars):
|
||||
----------------------------------------------------------
|
||||
ff
|
||||
Idiomas
|
||||
Código ISO 639-1 (ff) para el idioma fula.
|
||||
Música
|
||||
Fortissimo, notación musical para la intensidad de sonido.
|
||||
A la banda de heavy metal Fear Factory.
|
||||
A la banda de rock alternativo Foo Fighters.
|
||||
Videojuegos
|
||||
A la saga de videojuegos Final Fantasy.
|
||||
A el videojuego Garena Free Fire.
|
||||
Informática
|
||||
Follow Friday un término de la red social de microblogging Twitter.
|
||||
Sexualidad
|
||||
Fist fucking, práctica sexual.
|
||||
Proyectos Wikimedia
|
||||
Datos: Q219099
|
||||
Datos: Q219099
|
||||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'www') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=www&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: WWW (desambiguación)
|
||||
FECHA/HORA: 2025-12-05 18:29:24
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 756 chars):
|
||||
----------------------------------------------------------
|
||||
El término WWW, una sigla, puede referirse, en esta enciclopedia:
|
||||
a la World Wide Web (también, la Web) (que se usa de esas dos maneras, pero puede traducirse como "red mundial de redes de información" o "araña de información"), el sistema de documentos interconectados por enlaces de hipertexto, los cuales se ejecutan en Internet;
|
||||
a WorldWideWeb, el primer navegador en Internet;
|
||||
a Wild Wild West, una película cómica y de acción ambientada en el oeste, de 1999, dirigida por Barry Sonnenfeld y protagonizada por Will Smith, Kevin Kline, Kenneth Branagh y Salma Hayek;
|
||||
a WikiWikiWeb, el primero de todos los wikis, creado en marzo de 1995, para el sitio de Ward Cunningham, uno de los más importantes sitios donde discutir sobre patrones y antipatrones.
|
||||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'lle') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=lle&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: Љ
|
||||
FECHA/HORA: 2025-12-05 19:42:44
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 440 chars):
|
||||
----------------------------------------------------------
|
||||
La Lle o Lje (Љ, љ) es una ligadura de Л y Ь. Se usa en el Serbio y el Macedonio para representar al palatal lateral [ʎ], el mismo sonido que es representado en el español por Ll, cuando no hay yeísmo. Fue inventada por Vuk Stefanović Karadžić. En los alfabetos latinos serbio y croata es representada por el dígrafo Lj.
|
||||
Proyectos Wikimedia
|
||||
Datos: Q204782
|
||||
Multimedia: Cyrillic Lje / Q204782
|
||||
Datos: Q204782
|
||||
Multimedia: Cyrillic Lje / Q204782
|
||||
|
||||
|
||||
==========================================================
|
||||
== 🌐 DATOS EXTRAÍDOS (Búsqueda: 'cesar') ==
|
||||
==========================================================
|
||||
URL Artículo: https://es.wikipedia.org/w/index.php?search=cesar&title=Especial:Buscar&go=Ir
|
||||
TÍTULO: Cesar
|
||||
FECHA/HORA: 2025-12-05 19:43:14
|
||||
----------------------------------------------------------
|
||||
CONTENIDO (Primeros 1529 chars):
|
||||
----------------------------------------------------------
|
||||
Cesar es uno de los treinta y dos departamentos que forman la República de Colombia. Su capital es Valledupar. Está ubicado al noreste del país, en las regiones Andina y Caribe.
|
||||
El 21 de junio de 1967 el presidente Carlos Lleras Restrepo sancionó la ley por medio de la cual se creó el departamento del Cesar. De acuerdo con el documento, la entidad empezaría a funcionar 6 meses después, en diciembre de 1967.[7] Como capital fue designada Valledupar, ciudad que anteriormente ya había sido capital del departamento del Valle de Upar del Estado Soberano del Magdalena en 1864.[8]
|
||||
El Cesar es después de Antioquia y antes que Bogotá, el segundo departamento con mayores exportaciones de Colombia, con una cifra que ascendió a US$1,876 millones entre enero y agosto de 2017.[9]
|
||||
El departamento cuenta con asentamientos indígenas en estribaciones de la Sierra Nevada de Santa Marta, donde habitan los Arhuacos, Arzarios, Kankuamos, Koguis, Chimilas y en la serranía del Perijá, viven los Yucos.[10]
|
||||
Toponimia
|
||||
El nombre del departamento deriva del río con el mismo nombre río Cesar, y este último que es una derivación de Zesari, que en idioma chimila significa "Aguas calmadas".
|
||||
Historia
|
||||
Los primeros pobladores que llegaron a la región fueron los vástagos, los caribes y los arahuacos.[11]
|
||||
La conquista europea del territorio fue iniciada por el alemán Ambrosio Alfinger en 1530, quien llegó el valle de Upar haciendo frente a una considerable resistencia indígena.[12] En esa época, el territor
|
||||
[... Contenido truncado ...]
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# Módulo: vista/central_panel/view_alarmas.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from vista.config import *
|
||||
|
||||
|
||||
# AlarmManager se importa en PanelCentral y se pasa como argumento
|
||||
|
||||
|
||||
class AlarmaPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Alarmas (T4).
|
||||
Interfaz para programar y visualizar el tiempo restante de las alarmas.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_notebook, root, alarm_manager, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
|
||||
# Lógica de alarmas pasada desde el controlador
|
||||
self.alarm_manager = alarm_manager
|
||||
|
||||
self.after_alarm_id = None
|
||||
self.alarm_hours_entry = None
|
||||
self.alarm_minutes_entry = None
|
||||
self.alarm_seconds_entry = None
|
||||
self.scrollable_frame = None
|
||||
|
||||
self.crear_interfaz_alarmas(self)
|
||||
self.iniciar_actualizacion_alarmas()
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🔔 VISTA Y LÓGICA DE ALARMAS
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_alarmas(self, parent_frame):
|
||||
"""Crea la interfaz para programar y visualizar alarmas (H:M:S)."""
|
||||
|
||||
frame = ttk.Frame(parent_frame, padding=10, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame, text="Programar Nuevo Temporizador (H:M:S)", font=FUENTE_NEGOCIOS).pack(pady=(0, 10))
|
||||
|
||||
# --- Controles de Nueva Alarma (H:M:S) ---
|
||||
frame_input = ttk.Frame(frame, style='TFrame')
|
||||
frame_input.pack(fill='x', pady=5)
|
||||
|
||||
ttk.Label(frame_input, text="Horas:").pack(side='left', padx=(0, 2))
|
||||
self.alarm_hours_entry = ttk.Entry(frame_input, width=3);
|
||||
self.alarm_hours_entry.pack(side='left', padx=(0, 10))
|
||||
self.alarm_hours_entry.insert(0, "0")
|
||||
|
||||
ttk.Label(frame_input, text="Minutos:").pack(side='left', padx=(0, 2))
|
||||
self.alarm_minutes_entry = ttk.Entry(frame_input, width=3);
|
||||
self.alarm_minutes_entry.pack(side='left', padx=(0, 10))
|
||||
self.alarm_minutes_entry.insert(0, "1")
|
||||
|
||||
ttk.Label(frame_input, text="Segundos:").pack(side='left', padx=(0, 2))
|
||||
self.alarm_seconds_entry = ttk.Entry(frame_input, width=3);
|
||||
self.alarm_seconds_entry.pack(side='left', padx=(0, 15))
|
||||
self.alarm_seconds_entry.insert(0, "0")
|
||||
|
||||
ttk.Button(frame_input, text="➕ Crear Alarma", command=self.manejar_nueva_alarma,
|
||||
style='Action.TButton').pack(side='left')
|
||||
|
||||
ttk.Separator(frame, orient='horizontal').pack(fill='x', pady=15)
|
||||
|
||||
# --- Listado de Alarmas Activas ---
|
||||
ttk.Label(frame, text="Alarmas Activas (Tiempo Restante)", font=FUENTE_NEGOCIOS).pack(pady=(0, 5))
|
||||
|
||||
self.alarm_list_frame = ttk.Frame(frame)
|
||||
self.alarm_list_frame.pack(fill="both", expand=True)
|
||||
|
||||
canvas = tk.Canvas(self.alarm_list_frame, borderwidth=0, background=COLOR_BLANCO)
|
||||
vscroll = ttk.Scrollbar(self.alarm_list_frame, orient="vertical", command=canvas.yview)
|
||||
|
||||
self.scrollable_frame = ttk.Frame(canvas)
|
||||
|
||||
self.scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
|
||||
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=vscroll.set)
|
||||
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
vscroll.pack(side="right", fill="y")
|
||||
|
||||
def add_alarm_row(self, parent, alarm_data):
|
||||
"""Añade una fila con la info de la alarma y su botón de cancelación."""
|
||||
row_frame = ttk.Frame(parent, padding=5, style='Note.TFrame')
|
||||
row_frame.pack(fill='x', padx=5, pady=2)
|
||||
|
||||
total_s = alarm_data['total_seconds']
|
||||
h = total_s // 3600
|
||||
m = (total_s % 3600) // 60
|
||||
s = total_s % 60
|
||||
total_time_str = f"{h:02d}h:{m:02d}m:{s:02d}s"
|
||||
|
||||
info_text = (f"[ID{alarm_data['id']}] {alarm_data['restante']} -> {alarm_data['nombre']} "
|
||||
f"({total_time_str} total)")
|
||||
ttk.Label(row_frame, text=info_text, font=('Consolas', 10), style='Note.TLabel').pack(side='left', fill='x',
|
||||
expand=True)
|
||||
|
||||
ttk.Button(row_frame, text="❌ Cancelar", style='Danger.TButton', width=10,
|
||||
command=lambda id=alarm_data['id']: self.manejar_cancelar_alarma(id)).pack(side='right')
|
||||
|
||||
def manejar_nueva_alarma(self):
|
||||
"""Captura los datos del formulario (H:M:S) y llama al AlarmManager."""
|
||||
try:
|
||||
hours = int(self.alarm_hours_entry.get() or 0)
|
||||
minutes = int(self.alarm_minutes_entry.get() or 0)
|
||||
seconds = int(self.alarm_seconds_entry.get() or 0)
|
||||
|
||||
total_seconds = (hours * 3600) + (minutes * 60) + seconds
|
||||
|
||||
if total_seconds <= 0:
|
||||
print("⚠️ El tiempo de alarma debe ser un número positivo (H:M:S > 0).")
|
||||
return
|
||||
|
||||
self.alarm_manager.set_alarm(total_seconds)
|
||||
|
||||
# Limpiar campos
|
||||
self.alarm_hours_entry.delete(0, tk.END);
|
||||
self.alarm_hours_entry.insert(0, "0")
|
||||
self.alarm_minutes_entry.delete(0, tk.END);
|
||||
self.alarm_minutes_entry.insert(0, "1")
|
||||
self.alarm_seconds_entry.delete(0, tk.END);
|
||||
self.alarm_seconds_entry.insert(0, "0")
|
||||
|
||||
self.actualizar_lista_alarmas()
|
||||
|
||||
except ValueError:
|
||||
print("⚠️ Por favor, introduce números enteros válidos para el tiempo.")
|
||||
|
||||
def manejar_cancelar_alarma(self, alarm_id):
|
||||
"""Cancela la alarma usando su ID."""
|
||||
if self.alarm_manager.cancel_alarm(alarm_id):
|
||||
self.actualizar_lista_alarmas()
|
||||
|
||||
def actualizar_lista_alarmas(self):
|
||||
"""Actualiza la visualización de las alarmas activas."""
|
||||
if not self.scrollable_frame:
|
||||
self.after_alarm_id = self.after(1000, self.actualizar_lista_alarmas)
|
||||
return
|
||||
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
active_alarms = self.alarm_manager.get_active_alarms()
|
||||
|
||||
if not active_alarms:
|
||||
ttk.Label(self.scrollable_frame, text="--- No hay alarmas activas ---", font=('Consolas', 10),
|
||||
foreground=COLOR_TEXTO).pack(padx=10, pady=10)
|
||||
|
||||
for alarm in active_alarms:
|
||||
self.add_alarm_row(self.scrollable_frame, alarm)
|
||||
|
||||
self.after_alarm_id = self.after(1000, self.actualizar_lista_alarmas)
|
||||
|
||||
def iniciar_actualizacion_alarmas(self):
|
||||
"""Inicia el ciclo de actualización de la lista de alarmas."""
|
||||
self.after_alarm_id = self.after(0, self.actualizar_lista_alarmas)
|
||||
|
||||
def detener_actualizacion(self):
|
||||
"""Detiene el ciclo de actualización."""
|
||||
if self.after_alarm_id:
|
||||
self.after_cancel(self.after_alarm_id)
|
||||
self.after_alarm_id = None
|
||||
print("Ciclo de actualización de alarmas detenido.")
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
# Módulo: vista/central_panel/view_carrera.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import random
|
||||
from logica.T2.carreraCamellos import (
|
||||
iniciar_carrera,
|
||||
obtener_estado_carrera,
|
||||
RESULTADO_ULTIMO
|
||||
)
|
||||
from vista.config import *
|
||||
|
||||
|
||||
class CarreraPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Carrera (T2).
|
||||
Controla la simulación y visualización de la carrera de camellos.
|
||||
"""
|
||||
|
||||
INTERVALO_CARRERA_MS = 200
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
self.after_carrera_id = None
|
||||
self.camellos = []
|
||||
self.progreso_labels = {}
|
||||
self.frame_progreso = None
|
||||
self.carrera_estado_label = None
|
||||
self.carrera_info_label = None
|
||||
|
||||
self.crear_interfaz_carrera(self)
|
||||
self.iniciar_actualizacion_carrera() # Carga el resultado guardado al inicio
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🐪 VISTA DE CARRERA
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_carrera(self, parent_frame):
|
||||
"""Crea los controles y la visualización de la Carrera de Camellos."""
|
||||
frame_controles = ttk.Frame(parent_frame, style='TFrame', padding=10)
|
||||
frame_controles.pack(fill="x")
|
||||
|
||||
ttk.Label(frame_controles, text="Resultado de Carrera de Camellos (T2 Sincronización)",
|
||||
style='TLabel', font=FUENTE_NEGOCIOS).pack(side="left", padx=5)
|
||||
|
||||
self.carrera_estado_label = ttk.Label(frame_controles, text="Estado.", style='TLabel', font=FUENTE_NEGOCIOS)
|
||||
self.carrera_estado_label.pack(side="right", padx=10)
|
||||
|
||||
self.frame_progreso = ttk.Frame(parent_frame, style='TFrame', padding=10)
|
||||
self.frame_progreso.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(self.frame_progreso,
|
||||
text="Presiona el botón 'App2 (T2-Carrera 🏁)' en el panel lateral para iniciar la simulación de hilos.",
|
||||
style='TLabel').pack(pady=20)
|
||||
|
||||
def crear_visualizacion_carrera(self, nombres):
|
||||
"""Prepara el layout de la carrera (barras de progreso)."""
|
||||
for widget in self.frame_progreso.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
for i, nombre in enumerate(nombres):
|
||||
ttk.Label(self.frame_progreso, text=f"{nombre}: ", style='TLabel', font=FUENTE_NEGOCIOS).grid(row=i,
|
||||
column=0,
|
||||
sticky="w")
|
||||
|
||||
label_progreso = ttk.Label(self.frame_progreso, text="[Esperando...]", style='TLabel',
|
||||
foreground=COLOR_TEXTO)
|
||||
label_progreso.grid(row=i, column=1, sticky="w", padx=10)
|
||||
self.progreso_labels[nombre] = label_progreso
|
||||
|
||||
label_posicion = ttk.Label(self.frame_progreso, text="", style='TLabel')
|
||||
label_posicion.grid(row=i, column=2, sticky="w")
|
||||
self.progreso_labels[f'{nombre}_pos'] = label_posicion
|
||||
|
||||
self.carrera_info_label = ttk.Label(self.frame_progreso, text="", style='TLabel', font=FUENTE_NEGOCIOS)
|
||||
self.carrera_info_label.grid(row=len(nombres), column=0, columnspan=3, sticky="w", pady=(10, 0))
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# ⚙️ CONTROL DE ESTADO DE CARRERA
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def manejar_inicio_carrera(self):
|
||||
"""Inicia una nueva carrera de camellos con un número aleatorio de participantes."""
|
||||
if self.camellos and any(c.is_alive() for c in self.camellos):
|
||||
self.carrera_estado_label.config(text="⚠️ Ya hay una carrera en curso.")
|
||||
return
|
||||
|
||||
print("Iniciando Carrera de Camellos con número variable de participantes...")
|
||||
|
||||
num_camellos = random.randint(10, 20)
|
||||
nombres = [f"Camello {i + 1}" for i in range(num_camellos)]
|
||||
|
||||
self.progreso_labels = {}
|
||||
self.camellos = iniciar_carrera(nombres)
|
||||
self.carrera_estado_label.config(text=f"¡Carrera en marcha! 💨 ({num_camellos} participantes)")
|
||||
self.crear_visualizacion_carrera(nombres)
|
||||
self.iniciar_actualizacion_carrera()
|
||||
|
||||
def mostrar_progreso_activo(self, datos_activos):
|
||||
"""Actualiza la visualización de la carrera mientras los hilos están corriendo."""
|
||||
if not datos_activos['camellos']:
|
||||
return
|
||||
|
||||
for estado in datos_activos['camellos']:
|
||||
nombre = estado['nombre']
|
||||
progreso = estado['progreso']
|
||||
etiqueta_progreso = self.progreso_labels.get(nombre)
|
||||
etiqueta_posicion = self.progreso_labels.get(f'{nombre}_pos')
|
||||
|
||||
if etiqueta_progreso:
|
||||
barra = "█" * (progreso // 2)
|
||||
texto_progreso = f"[{barra}{' ' * (25 - len(barra))}] ({progreso}/50) Estado: {estado['estado']}"
|
||||
etiqueta_progreso.config(text=texto_progreso)
|
||||
|
||||
if etiqueta_posicion and estado['posicion']:
|
||||
etiqueta_posicion.config(text=f"POS: {estado['posicion']} 🏆", foreground=COLOR_ACCION)
|
||||
|
||||
self.carrera_estado_label.config(text="Carrera en curso...")
|
||||
self.carrera_info_label.config(text="")
|
||||
|
||||
def mostrar_resultado_final(self, resultado_final):
|
||||
"""Muestra el resultado final persistente de la carrera."""
|
||||
nombres_finales = [c['nombre'] for c in resultado_final['camellos']]
|
||||
if not self.progreso_labels or any(n not in self.progreso_labels for n in nombres_finales):
|
||||
self.crear_visualizacion_carrera(nombres_finales)
|
||||
|
||||
camellos_ordenados = sorted(resultado_final['camellos'], key=lambda x: x['posicion'])
|
||||
|
||||
for estado in camellos_ordenados:
|
||||
nombre = estado['nombre']
|
||||
etiqueta_progreso = self.progreso_labels.get(nombre)
|
||||
etiqueta_posicion = self.progreso_labels.get(f'{nombre}_pos')
|
||||
|
||||
if etiqueta_progreso:
|
||||
barra = "█" * (estado['progreso'] // 2)
|
||||
texto_progreso = f"[{barra}{' ' * (25 - len(barra))}] (50/50) Estado: Meta"
|
||||
etiqueta_progreso.config(text=texto_progreso)
|
||||
|
||||
if etiqueta_posicion:
|
||||
etiqueta_posicion.config(text=f"POS: {estado['posicion']} {'🏆' if estado['posicion'] == 1 else ''}",
|
||||
foreground=COLOR_ACCION)
|
||||
|
||||
self.carrera_estado_label.config(text="✅ Carrera Terminada.")
|
||||
self.carrera_info_label.config(text=f"¡El ganador es: {resultado_final['ganador']}!",
|
||||
font=FUENTE_TITULO, foreground=COLOR_EXITO)
|
||||
|
||||
def actualizar_carrera(self):
|
||||
"""Ciclo de actualización visual."""
|
||||
estado = obtener_estado_carrera(self.camellos)
|
||||
|
||||
if estado['tipo'] == 'final':
|
||||
self.mostrar_resultado_final(estado['datos'])
|
||||
self.detener_actualizacion_carrera()
|
||||
return
|
||||
|
||||
elif estado['tipo'] == 'activo':
|
||||
self.mostrar_progreso_activo(estado['datos'])
|
||||
|
||||
self.after_carrera_id = self.after(self.INTERVALO_CARRERA_MS, self.actualizar_carrera)
|
||||
|
||||
def iniciar_actualizacion_carrera(self):
|
||||
"""Inicia el ciclo de actualización visual (o carga el resultado guardado al inicio)."""
|
||||
self.detener_actualizacion_carrera()
|
||||
|
||||
# Si hay un resultado final guardado y no hay carrera activa, mostrarlo.
|
||||
if RESULTADO_ULTIMO and not RESULTADO_ULTIMO.get('activa', True) and self.frame_progreso:
|
||||
self.mostrar_resultado_final(RESULTADO_ULTIMO)
|
||||
else:
|
||||
self.carrera_estado_label.config(text="Carrera lista para empezar.")
|
||||
self.after_carrera_id = self.after(0, self.actualizar_carrera)
|
||||
|
||||
def detener_actualizacion_carrera(self):
|
||||
"""Detiene el ciclo de actualización visual de la carrera."""
|
||||
if self.after_carrera_id:
|
||||
self.after_cancel(self.after_carrera_id)
|
||||
self.after_carrera_id = None
|
||||
print("Ciclo de actualización de carrera detenido.")
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Módulo: vista/central_panel/view_chat.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from vista.config import *
|
||||
from datetime import datetime
|
||||
import random # Para simular respuestas
|
||||
|
||||
|
||||
class ChatPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Chat.
|
||||
Proporciona una interfaz básica para el chat (historial, entrada, envío).
|
||||
"""
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
self.chat_history = None
|
||||
self.chat_input_entry = None
|
||||
self.crear_interfaz_chat(self)
|
||||
self.agregar_mensaje_sistema("Bienvenido al sistema de Chat. Introduce un mensaje para empezar.")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 💬 VISTA Y FUNCIONALIDAD BÁSICA DEL CHAT
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_chat(self, parent_frame):
|
||||
"""Crea el área de historial, el campo de entrada y el botón de envío."""
|
||||
|
||||
frame = ttk.Frame(parent_frame, padding=15, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
frame.grid_rowconfigure(0, weight=1) # Historial de Chat
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 1. Área de Historial de Mensajes (Text Widget)
|
||||
self.chat_history = tk.Text(
|
||||
frame,
|
||||
height=20,
|
||||
wrap="word",
|
||||
bg=COLOR_BLANCO,
|
||||
fg=COLOR_TEXTO,
|
||||
relief="flat",
|
||||
borderwidth=1,
|
||||
font=('Arial', 10)
|
||||
)
|
||||
self.chat_history.grid(row=0, column=0, columnspan=2, sticky="nsew", pady=(0, 10))
|
||||
self.chat_history.config(state=tk.DISABLED) # No editable
|
||||
|
||||
# 2. Frame para la entrada y el botón
|
||||
frame_input = ttk.Frame(frame, style='TFrame')
|
||||
frame_input.grid(row=1, column=0, columnspan=2, sticky="ew")
|
||||
|
||||
frame_input.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# Campo de Entrada de Texto
|
||||
self.chat_input_entry = ttk.Entry(frame_input, font=('Arial', 11))
|
||||
self.chat_input_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5))
|
||||
self.chat_input_entry.bind('<Return>', self.manejar_envio_mensaje)
|
||||
|
||||
# Botón de Enviar
|
||||
ttk.Button(frame_input, text="Enviar 🚀", command=self.manejar_envio_mensaje,
|
||||
style='Action.TButton').grid(row=0, column=1, sticky="e")
|
||||
|
||||
def agregar_mensaje(self, remitente, texto):
|
||||
"""Añade un mensaje al historial de chat."""
|
||||
|
||||
# Habilitar temporalmente para la inserción
|
||||
self.chat_history.config(state=tk.NORMAL)
|
||||
|
||||
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
# Formato del mensaje (ej. [16:55:01] TÚ: Hola mundo)
|
||||
mensaje_completo = f"[{timestamp}] {remitente.upper()}: {texto.strip()}\n"
|
||||
|
||||
self.chat_history.insert(tk.END, mensaje_completo)
|
||||
|
||||
# Deshabilitar de nuevo y hacer scroll al final
|
||||
self.chat_history.config(state=tk.DISABLED)
|
||||
self.chat_history.see(tk.END)
|
||||
|
||||
def agregar_mensaje_sistema(self, texto):
|
||||
"""Añade un mensaje específico del sistema (para inicio o errores)."""
|
||||
self.agregar_mensaje("SISTEMA", f"--- {texto} ---")
|
||||
|
||||
def simular_respuesta_bot(self, mensaje_usuario):
|
||||
"""
|
||||
Función placeholder para simular la respuesta de un bot/asistente.
|
||||
Aquí es donde conectarías tu lógica de LLM, socket o backend de chat real.
|
||||
"""
|
||||
respuestas = [
|
||||
"Entendido. Procesando tu solicitud...",
|
||||
"Gracias por tu mensaje. El chat está en desarrollo.",
|
||||
"Respuesta automática: ¿Cómo puedo ayudarte con las tareas de T1 a T4?",
|
||||
f"El tema '{mensaje_usuario.split()[0]}' es muy interesante, pero no tengo datos al respecto.",
|
||||
]
|
||||
respuesta = random.choice(respuestas)
|
||||
self.agregar_mensaje("ASISTENTE", respuesta)
|
||||
|
||||
def manejar_envio_mensaje(self, event=None):
|
||||
"""Procesa el mensaje del usuario al presionar Enter o el botón Enviar."""
|
||||
mensaje = self.chat_input_entry.get()
|
||||
|
||||
if mensaje.strip():
|
||||
self.agregar_mensaje("TÚ", mensaje)
|
||||
self.chat_input_entry.delete(0, tk.END) # Limpiar campo de entrada
|
||||
|
||||
# Simular la respuesta del sistema después de un breve delay
|
||||
self.after(500, lambda: self.simular_respuesta_bot(mensaje))
|
||||
|
||||
# Prevenir que el evento 'Return' propague y cause errores si se llama
|
||||
return "break" if event else None
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Módulo: vista/central_panel/view_correos.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from vista.config import *
|
||||
|
||||
|
||||
class CorreosPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Correos.
|
||||
Placeholder para la gestión de correos electrónicos y notificaciones.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
self.crear_interfaz_correos(self)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📧 VISTA PLACEHOLDER
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_correos(self, parent_frame):
|
||||
"""Crea un mensaje placeholder para la pestaña de Correos."""
|
||||
frame = ttk.Frame(parent_frame, padding=20, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame, text="Gestor de Correos Electrónicos 📧",
|
||||
font=FUENTE_TITULO).pack(pady=(10, 20))
|
||||
|
||||
ttk.Label(frame, text="🛠️ ESTA FUNCIÓN ESTÁ EN DESARROLLO 🛠️",
|
||||
font=FUENTE_NEGOCIOS, foreground=COLOR_ACCION).pack(pady=10)
|
||||
|
||||
ttk.Label(frame,
|
||||
text="Aquí se integrará la visualización y gestión de emails o un sistema de notificaciones por correo.",
|
||||
wraplength=500, justify=tk.CENTER).pack(pady=5)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Módulo: vista/central_panel/view_enlaces.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from vista.config import *
|
||||
|
||||
|
||||
class EnlacesPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Enlaces.
|
||||
Placeholder para la gestión de accesos directos y enlaces rápidos.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
self.crear_interfaz_enlaces(self)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🔗 VISTA PLACEHOLDER
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_enlaces(self, parent_frame):
|
||||
"""Crea un mensaje placeholder para la pestaña de Enlaces."""
|
||||
frame = ttk.Frame(parent_frame, padding=20, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame, text="Gestor de Enlaces Rápidos 🔗",
|
||||
font=FUENTE_TITULO).pack(pady=(10, 20))
|
||||
|
||||
ttk.Label(frame, text="🛠️ ESTA FUNCIÓN ESTÁ EN DESARROLLO 🛠️",
|
||||
font=FUENTE_NEGOCIOS, foreground=COLOR_ACCION).pack(pady=10)
|
||||
|
||||
ttk.Label(frame,
|
||||
text="Utilice este panel para almacenar y acceder rápidamente a URLs o recursos externos importantes para sus tareas.",
|
||||
wraplength=500, justify=tk.CENTER).pack(pady=5)
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# Módulo: vista/central_panel/view_notas.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
# Asumo que la ruta de importación de la lógica es correcta
|
||||
from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes
|
||||
from vista.config import *
|
||||
|
||||
|
||||
class NotasPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Tareas (Editor de Notas).
|
||||
"""
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
self.notes_text_editor = None
|
||||
|
||||
# 🎯 CORRECCIÓN CRÍTICA: Configurar la expansión del Frame de la pestaña
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.crear_interfaz_tareas(self)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📄 VISTA Y LÓGICA DE NOTAS
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_tareas(self, parent_frame):
|
||||
"""Crea el editor de texto simple para el archivo res/notes."""
|
||||
|
||||
# 1. Frame Contenedor Principal. Lo ubicamos con grid para que ocupe todo el espacio.
|
||||
frame = ttk.Frame(parent_frame, padding=15, style='TFrame')
|
||||
# Usamos grid para ocupar el único espacio disponible en el parent_frame (NotasPanel)
|
||||
frame.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# 2. Configuración de expansión dentro del Frame Contenedor
|
||||
# Queremos que el Text widget sea el único que se expanda vertical y horizontalmente.
|
||||
# El Text widget está en la fila 2 (después de 2 labels).
|
||||
frame.grid_rowconfigure(2, weight=1) # Fila del Text widget
|
||||
frame.grid_columnconfigure(0, weight=1) # Columna única
|
||||
|
||||
# Etiquetas (usan pack/grid si fuera necesario, pero aquí usaremos grid para simplificar)
|
||||
ttk.Label(frame, text="Editor de Notas ", font=FUENTE_TITULO).grid(row=0, column=0, pady=(0, 10), sticky="w")
|
||||
ttk.Label(frame, text="Use este panel para tomar notas rápidas sobre la ejecución de tareas.",
|
||||
font=FUENTE_NEGOCIOS).grid(row=1, column=0, pady=(0, 15), sticky="w")
|
||||
|
||||
# Editor de Texto
|
||||
self.notes_text_editor = tk.Text(
|
||||
frame,
|
||||
height=20,
|
||||
wrap="word",
|
||||
bg=COLOR_BLANCO,
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
font=FUENTE_MONO
|
||||
)
|
||||
# 🎯 Ubicación del Text widget en la fila que tiene weight=1
|
||||
self.notes_text_editor.grid(row=2, column=0, sticky="nsew", pady=(0, 10))
|
||||
|
||||
# Frame de Botones
|
||||
frame_botones = ttk.Frame(frame)
|
||||
frame_botones.grid(row=3, column=0, sticky="ew", pady=(5, 0)) # Colocado debajo del Text widget
|
||||
|
||||
# Botones dentro del Frame de Botones (usando pack para alineación lateral)
|
||||
ttk.Button(frame_botones, text="Guardar Cambios", command=self.guardar_res_notes, style='Action.TButton').pack(
|
||||
side=tk.RIGHT)
|
||||
ttk.Button(frame_botones, text="Cargar Archivo", command=self.cargar_res_notes, style='Action.TButton').pack(
|
||||
side=tk.LEFT)
|
||||
|
||||
self.cargar_res_notes(initial_load=True)
|
||||
|
||||
def cargar_res_notes(self, initial_load=False):
|
||||
"""Carga el contenido de res/notes al editor de texto."""
|
||||
if not self.notes_text_editor: return
|
||||
|
||||
contenido = cargar_contenido_res_notes()
|
||||
self.notes_text_editor.delete("1.0", tk.END)
|
||||
|
||||
if "Error al cargar:" in contenido:
|
||||
self.notes_text_editor.insert(tk.END, contenido)
|
||||
else:
|
||||
if initial_load and not contenido.strip():
|
||||
self.notes_text_editor.insert(tk.END, "# Escriba aquí sus notas (res/notes)")
|
||||
else:
|
||||
self.notes_text_editor.insert(tk.END, contenido)
|
||||
|
||||
def guardar_res_notes(self):
|
||||
"""Guarda el contenido del editor de texto en res/notes."""
|
||||
if not self.notes_text_editor: return
|
||||
|
||||
contenido = self.notes_text_editor.get("1.0", tk.END)
|
||||
success, message = guardar_contenido_res_notes(contenido)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("✅ Guardado", "Notas guardadas exitosamente.")
|
||||
else:
|
||||
messagebox.showerror("❌ Error al Guardar", message)
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# Módulo: vista/central_panel/view_radio.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import json
|
||||
from vista.config import *
|
||||
|
||||
# Bloque para manejar la dependencia de VLC
|
||||
try:
|
||||
from logica.T2.musicReproductor import MusicReproductor
|
||||
except ImportError:
|
||||
print("⚠️ Error al importar MusicReproductor. Usando simulador.")
|
||||
|
||||
|
||||
# CLASE SIMULADA
|
||||
class MusicReproductor:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
|
||||
def ajustar_volumen(self, valor): print(f"🎶 SIMULANDO VOLUMEN: {valor}")
|
||||
|
||||
def cargar_y_reproducir(self, url): print(f"🎶 SIMULANDO PLAY: {url}")
|
||||
|
||||
def reproducir(self): print("🎶 SIMULANDO PLAY")
|
||||
|
||||
def pausar(self, *args): print("🎶 SIMULANDO PAUSA")
|
||||
|
||||
def detener(self): print("🎶 SIMULANDO DETENER")
|
||||
|
||||
|
||||
class RadioPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Radios (T3).
|
||||
Gestiona la selección de emisoras y los controles de reproducción.
|
||||
"""
|
||||
|
||||
NOMBRE_FICHERO_RADIOS = "res/radios.json"
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
|
||||
self.emisoras_cargadas = self.cargar_emisoras()
|
||||
self.radio_seleccionada = tk.StringVar(value="---")
|
||||
self.volumen_var = tk.DoubleVar(value=50.0)
|
||||
|
||||
# Inicialización de la lógica del reproductor
|
||||
self.reproductor = MusicReproductor(initial_volume=self.volumen_var.get())
|
||||
|
||||
self.crear_interfaz_radios(self)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📻 VISTA Y LÓGICA DE RADIO
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def cargar_emisoras(self):
|
||||
"""Carga la lista de emisoras desde el archivo radios.json."""
|
||||
try:
|
||||
with open(self.NOMBRE_FICHERO_RADIOS, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"❌ Archivo de emisoras no encontrado en: '{self.NOMBRE_FICHERO_RADIOS}'.")
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
print(f"⚠️ Error al leer el archivo {self.NOMBRE_FICHERO_RADIOS}. Está mal formado.")
|
||||
return []
|
||||
|
||||
def crear_interfaz_radios(self, parent_frame):
|
||||
"""Crea la interfaz para seleccionar la emisora de radio."""
|
||||
|
||||
frame_radio = ttk.Frame(parent_frame, padding=10, style='TFrame')
|
||||
frame_radio.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame_radio, text="Selección de Emisoras de Radio", font=FUENTE_TITULO).pack(pady=10)
|
||||
|
||||
if not self.emisoras_cargadas:
|
||||
ttk.Label(frame_radio,
|
||||
text=f"No se encontraron emisoras en '{self.NOMBRE_FICHERO_RADIOS}'.",
|
||||
foreground='red').pack(pady=20)
|
||||
return
|
||||
|
||||
frame_listado = ttk.Frame(frame_radio)
|
||||
frame_listado.pack(fill="both", expand=True)
|
||||
|
||||
listbox = tk.Listbox(frame_listado, height=15, width=60, font=('Arial', 10),
|
||||
bg=COLOR_BLANCO, fg=COLOR_TEXTO, selectbackground=COLOR_ACCION)
|
||||
listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
|
||||
|
||||
scrollbar = ttk.Scrollbar(frame_listado, orient="vertical", command=listbox.yview)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
listbox.config(yscrollcommand=scrollbar.set)
|
||||
|
||||
for emisora in self.emisoras_cargadas:
|
||||
nombre_display = f"{emisora['nombre']} ({emisora.get('genero', 'N/D')})"
|
||||
listbox.insert(tk.END, nombre_display)
|
||||
|
||||
listbox.bind('<<ListboxSelect>>', lambda e: self.seleccionar_radio(listbox))
|
||||
|
||||
ttk.Label(frame_radio, text="URL del Stream:", font=FUENTE_NEGOCIOS).pack(pady=(10, 0), anchor="w")
|
||||
self.url_seleccionada_label = ttk.Label(frame_radio, text="N/A", wraplength=400, foreground=COLOR_TEXTO)
|
||||
self.url_seleccionada_label.pack(anchor="w")
|
||||
|
||||
# Controles de Volumen y Play/Pause
|
||||
frame_controles = ttk.Frame(frame_radio, padding=5)
|
||||
frame_controles.pack(fill="x", pady=10)
|
||||
|
||||
ttk.Button(frame_controles, text="▶️ Play", command=lambda: self.controlar_reproduccion('play'),
|
||||
style='Action.TButton').pack(side='left', padx=5)
|
||||
ttk.Button(frame_controles, text="⏸️ Pause", command=lambda: self.controlar_reproduccion('pause'),
|
||||
style='Action.TButton').pack(side='left', padx=5)
|
||||
|
||||
ttk.Label(frame_controles, textvariable=self.radio_seleccionada, font=FUENTE_NEGOCIOS).pack(side='left',
|
||||
padx=15)
|
||||
|
||||
ttk.Label(frame_controles, text="Volumen:").pack(side='right', padx=5)
|
||||
volumen_slider = ttk.Scale(frame_controles, from_=0, to=100, orient=tk.HORIZONTAL,
|
||||
variable=self.volumen_var, command=self.cambiar_volumen, length=100)
|
||||
volumen_slider.pack(side='right', padx=5)
|
||||
|
||||
def seleccionar_radio(self, listbox):
|
||||
"""Captura la selección y llama al reproductor para iniciar la reproducción."""
|
||||
seleccion = listbox.curselection()
|
||||
if seleccion:
|
||||
indice = seleccion[0]
|
||||
emisora = self.emisoras_cargadas[indice]
|
||||
url = emisora['url_stream']
|
||||
|
||||
self.radio_seleccionada.set(emisora['nombre'])
|
||||
self.url_seleccionada_label.config(text=url)
|
||||
self.reproductor.cargar_y_reproducir(url)
|
||||
|
||||
def controlar_reproduccion(self, accion):
|
||||
"""Llama al método de control del reproductor (play/pause)."""
|
||||
if accion == 'play':
|
||||
self.reproductor.reproducir()
|
||||
elif accion == 'pause':
|
||||
self.reproductor.pausar()
|
||||
|
||||
def cambiar_volumen(self, valor):
|
||||
"""Ajusta el volumen."""
|
||||
valor_entero = int(float(valor))
|
||||
self.volumen_var.set(valor_entero)
|
||||
self.reproductor.ajustar_volumen(valor_entero)
|
||||
|
||||
def detener_actualizacion(self):
|
||||
"""Método llamado por PanelCentral al cerrar la aplicación."""
|
||||
if self.reproductor:
|
||||
self.reproductor.detener()
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Módulo: vista/central_panel/view_recursos.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # <-- ¡IMPORTACIÓN NECESARIA!
|
||||
from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos
|
||||
from vista.config import * # Asumiendo que las constantes de estilo/fondo están aquí
|
||||
|
||||
|
||||
class RecursosPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de la pestaña Recursos (T1).
|
||||
Muestra el gráfico de Matplotlib con el consumo de recursos de red.
|
||||
"""
|
||||
|
||||
# NOTA: Los parámetros 'canvas' y 'canvas_widget' ya no son necesarios
|
||||
# en el constructor, pero los mantenemos para no romper el flujo de PanelCentral.
|
||||
def __init__(self, parent_notebook, figure, canvas, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.figure = figure
|
||||
# Aseguramos que el frame se expanda para llenar la pestaña
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.canvas = None # Lo crearemos aquí
|
||||
self.canvas_widget = None # Lo crearemos aquí
|
||||
self.grafico_frame = None
|
||||
|
||||
self.crear_interfaz_recursos(self)
|
||||
|
||||
# Estado inicial del gráfico
|
||||
crear_grafico_recursos(self.figure)
|
||||
|
||||
# Después de la creación, nos aseguramos de que PanelCentral obtenga las referencias
|
||||
# que espera si las necesita.
|
||||
|
||||
def crear_interfaz_recursos(self, parent_frame):
|
||||
"""Prepara el Frame para contener el gráfico de Matplotlib."""
|
||||
|
||||
# 1. Crear el Frame que contendrá el Canvas (contenedor interno)
|
||||
self.grafico_frame = ttk.Frame(parent_frame, style='TFrame')
|
||||
self.grafico_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
|
||||
self.grafico_frame.grid_rowconfigure(0, weight=1)
|
||||
self.grafico_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# 2. 🎯 CREAR Y UBICAR EL CANVAS DENTRO DEL FRAME (SOLUCIÓN DEL PROBLEMA)
|
||||
# El canvas debe crearse AQUÍ y usar 'self.grafico_frame' como master
|
||||
self.canvas = FigureCanvasTkAgg(self.figure, master=self.grafico_frame)
|
||||
self.canvas_widget = self.canvas.get_tk_widget()
|
||||
|
||||
# Usamos .grid() para llenar el frame_contenedor
|
||||
self.canvas_widget.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
||||
# 📞 MÉTODOS DE CONEXIÓN (Llamados por PanelCentral)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def actualizar_datos(self, net_in, net_out):
|
||||
"""Recibe los datos del monitor de red y actualiza el historial."""
|
||||
actualizar_historial_datos(net_in, net_out)
|
||||
|
||||
def dibujar_grafico(self):
|
||||
"""Llama a la función de redibujo del gráfico."""
|
||||
if self.figure:
|
||||
crear_grafico_recursos(self.figure)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Módulo: vista/central_panel/view_scrapping.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
from vista.config import *
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NavegadorPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de visualización de contenido web (Scraping).
|
||||
Contiene un widget Text para mostrar los resultados de las extracciones.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_notebook, root, *args, **kwargs):
|
||||
super().__init__(parent_notebook, *args, **kwargs)
|
||||
self.root = root
|
||||
self.web_text_viewer = None
|
||||
self.crear_interfaz_navegador(self)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🌐 VISTA DE NAVEGADOR
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_navegador(self, parent_frame):
|
||||
"""Crea el área donde se mostrará el resultado del scraping."""
|
||||
|
||||
frame = ttk.Frame(parent_frame, padding=15, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame, text="Visualizador de Contenido Web (Scraping)", font=FUENTE_TITULO).pack(pady=(0, 10),
|
||||
anchor="w")
|
||||
ttk.Label(frame, text="El resultado de la extracción de datos se carga aquí.",
|
||||
font=FUENTE_NEGOCIOS).pack(pady=(0, 15), anchor="w")
|
||||
|
||||
self.web_text_viewer = tk.Text(
|
||||
frame,
|
||||
height=20,
|
||||
wrap="word",
|
||||
bg=COLOR_BLANCO,
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
font=FUENTE_MONO
|
||||
)
|
||||
self.web_text_viewer.pack(fill="both", expand=True, pady=(0, 10))
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📞 MÉTODO DE CONEXIÓN
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def cargar_contenido_web(self, titulo, contenido):
|
||||
"""Carga el contenido extraído (scraping) al widget de texto."""
|
||||
if not self.web_text_viewer:
|
||||
messagebox.showerror("Error", "El visualizador web no está inicializado.")
|
||||
return
|
||||
|
||||
self.web_text_viewer.delete("1.0", tk.END)
|
||||
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
header = f"--- RESULTADO DE EXTRACCIÓN: {titulo.upper()} ({timestamp}) ---\n\n"
|
||||
self.web_text_viewer.insert(tk.END, header)
|
||||
self.web_text_viewer.insert(tk.END, contenido)
|
||||
self.web_text_viewer.see("1.0")
|
||||
|
|
@ -1,380 +1,169 @@
|
|||
# Módulo: vista/panel_central.py
|
||||
|
||||
import random
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
# --- LÓGICA DE T1 (MONITOR DE RECURSOS) ---
|
||||
from logica.T1.trafficMeter import iniciar_monitor_red
|
||||
from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos
|
||||
from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes # <--- AÑADIDO
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
# --- LÓGICA DE T2 (CARRERA DE CAMELLOS) ---
|
||||
from logica.T2.carreraCamellos import (
|
||||
iniciar_carrera,
|
||||
obtener_estado_carrera,
|
||||
RESULTADO_ULTIMO
|
||||
)
|
||||
|
||||
# --- LÓGICA DE T3 (REPRODUCTOR DE MÚSICA) ---
|
||||
# Bloque para manejar la dependencia de VLC
|
||||
try:
|
||||
from logica.T2.musicReproductor import MusicReproductor
|
||||
except ImportError:
|
||||
print("⚠️ Error al importar MusicReproductor. Usando simulador.")
|
||||
|
||||
|
||||
class MusicReproductor:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
|
||||
def ajustar_volumen(self, valor): pass
|
||||
|
||||
def cargar_y_reproducir(self, url): print(f"🎶 SIMULANDO PLAY: {url}")
|
||||
|
||||
def reproducir(self): pass
|
||||
|
||||
def pausar(self, *args): pass
|
||||
|
||||
def detener(self): pass
|
||||
|
||||
# 🟢 LÓGICA DE T4 (ALARMAS)
|
||||
# --- LÓGICA DE CONTROL UNIVERSAL ---
|
||||
from logica.T1.trafficMeter import iniciar_monitor_red
|
||||
from logica.T2.alarm import AlarmManager
|
||||
|
||||
# --- MÓDULOS DE PESTAÑAS (Importación de las Vistas Modulares) ---
|
||||
from vista.central_panel.view_recursos import RecursosPanel
|
||||
from vista.central_panel.view_carrera import CarreraPanel
|
||||
from vista.central_panel.view_radio import RadioPanel
|
||||
from vista.central_panel.view_alarmas import AlarmaPanel
|
||||
from vista.central_panel.view_notas import NotasPanel
|
||||
from vista.central_panel.view_scrapping import NavegadorPanel
|
||||
from vista.central_panel.view_chat import ChatPanel
|
||||
from vista.central_panel.view_correos import CorreosPanel
|
||||
from vista.central_panel.view_enlaces import EnlacesPanel
|
||||
|
||||
# --- IMPORTACIÓN UNIVERSAL DE CONSTANTES ---
|
||||
from vista.config import *
|
||||
|
||||
|
||||
class PanelCentral(ttk.Frame):
|
||||
"""Contiene el Notebook (subpestañas), el panel de Notas y el panel de Chat,
|
||||
y gestiona directamente la lógica de control de T1, T2, T3 y T4."""
|
||||
"""
|
||||
Controlador central del Panel.
|
||||
Gestiona el layout principal, inicializa la lógica universal y coordina
|
||||
las instancias de las clases modulares de cada pestaña (view_*).
|
||||
"""
|
||||
|
||||
INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS
|
||||
INTERVALO_CARRERA_MS = 200
|
||||
|
||||
NOMBRE_FICHERO_RADIOS = "res/radios.json"
|
||||
|
||||
def __init__(self, parent, root, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
self.root = root
|
||||
|
||||
# --- Variables de Estado y Lógica Central ---
|
||||
self.after_id = None
|
||||
self.after_carrera_id = None
|
||||
self.after_alarm_id = None
|
||||
self.net_monitor = None
|
||||
|
||||
# T2
|
||||
self.camellos = []
|
||||
self.progreso_labels = {}
|
||||
self.frame_carrera_controles = None
|
||||
self.frame_progreso = None
|
||||
self.carrera_info_label = None
|
||||
self.carrera_estado_label = None
|
||||
|
||||
# T1
|
||||
self.net_monitor = iniciar_monitor_red()
|
||||
# Lógica de Matplotlib (T1)
|
||||
self.figure = Figure(figsize=(5, 4), dpi=100)
|
||||
self.canvas = None
|
||||
self.canvas_widget = None
|
||||
self.grafico_frame = None
|
||||
|
||||
# T3 (Radios)
|
||||
self.emisoras_cargadas = self.cargar_emisoras()
|
||||
self.radio_seleccionada = tk.StringVar(value="---")
|
||||
self.volumen_var = tk.DoubleVar(value=50.0)
|
||||
self.reproductor = MusicReproductor(initial_volume=self.volumen_var.get())
|
||||
|
||||
# 🟢 T4 (Alarmas) - Inicialización de variables UI.
|
||||
self.alarm_manager = None
|
||||
self.alarm_list_frame = None
|
||||
self.scrollable_frame = None
|
||||
self.alarm_hours_entry = None
|
||||
self.alarm_minutes_entry = None
|
||||
self.alarm_seconds_entry = None
|
||||
|
||||
# 📄 Tareas (res/notes)
|
||||
self.notes_text_editor = None # <--- AÑADIDO
|
||||
|
||||
# 2. CONFIGURACIÓN DEL LAYOUT
|
||||
self.grid_columnconfigure(0, weight=3)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
self.crear_area_principal() # <--- FUNCIÓN SIMPLIFICADA
|
||||
self.crear_panel_chat_y_alumnos()
|
||||
|
||||
# Inicializar el AlarmManager solo después de que show_alarm_popup esté definido.
|
||||
self.inicializar_alarmas()
|
||||
|
||||
# 3. INICIO DE CICLOS DE ACTUALIZACIÓN
|
||||
self.iniciar_actualizacion_automatica()
|
||||
self.iniciar_actualizacion_carrera()
|
||||
self.iniciar_actualizacion_alarmas()
|
||||
|
||||
def inicializar_alarmas(self):
|
||||
"""Inicializa AlarmManager, pasándole el método de callback show_alarm_popup."""
|
||||
# Lógica de Alarmas
|
||||
self.alarm_manager = AlarmManager(self.root, self.show_alarm_popup)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📻 LÓGICA Y VISTA DE T3 (REPRODUCTOR DE RADIOS)
|
||||
# ... (El código de cargar_emisoras, crear_interfaz_radios, seleccionar_radio, controlar_reproduccion, cambiar_volumen no tiene cambios)
|
||||
# Contenedores para módulos y pestañas
|
||||
self.tabs = {}
|
||||
self.modulos = {}
|
||||
|
||||
# Configuración de Layout Principal
|
||||
# 🎯 CORRECCIÓN 1: Se elimina la columna 1. La columna 0 ocupa todo el espacio.
|
||||
self.grid_columnconfigure(0, weight=1) # La Columna de Pestañas ahora es la única y principal
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
self.crear_area_principal()
|
||||
# ❌ Se elimina la llamada a self.crear_panel_chat_y_alumnos()
|
||||
|
||||
# 📦 ESTRUCTURA PRINCIPAL Y CREACIÓN DE PESTAÑAS
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def cargar_emisoras(self):
|
||||
"""Carga la lista de emisoras desde el archivo radios.json."""
|
||||
try:
|
||||
with open(self.NOMBRE_FICHERO_RADIOS, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"❌ Archivo de emisoras no encontrado en: '{self.NOMBRE_FICHERO_RADIOS}'.")
|
||||
return []
|
||||
except json.JSONDecodeError:
|
||||
print(f"⚠️ Error al leer el archivo {self.NOMBRE_FICHERO_RADIOS}. Está mal formado.")
|
||||
return []
|
||||
def crear_area_principal(self):
|
||||
"""Crea el contenedor de las subpestañas (Notebook), columna izquierda (0)."""
|
||||
frame_izquierdo = ttk.Frame(self, style='TFrame')
|
||||
frame_izquierdo.grid(row=0, column=0, sticky="nsew") # Ocupa toda la ventana
|
||||
|
||||
def crear_interfaz_radios(self, parent_frame):
|
||||
"""Crea la interfaz para seleccionar la emisora de radio."""
|
||||
frame_izquierdo.grid_rowconfigure(0, weight=1)
|
||||
frame_izquierdo.grid_columnconfigure(0, weight=1)
|
||||
|
||||
frame_radio = ttk.Frame(parent_frame, padding=10, style='TFrame')
|
||||
frame_radio.pack(expand=True, fill="both")
|
||||
self.crear_notebook_pestanas(frame_izquierdo)
|
||||
|
||||
ttk.Label(frame_radio, text="Selección de Emisoras de Radio", font=FUENTE_TITULO).pack(pady=10)
|
||||
def crear_notebook_pestanas(self, parent_frame):
|
||||
"""Crea las pestañas e inicializa los módulos modulares en cada Frame."""
|
||||
sub_notebook = ttk.Notebook(parent_frame)
|
||||
sub_notebook.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
if not self.emisoras_cargadas:
|
||||
ttk.Label(frame_radio,
|
||||
text=f"No se encontraron emisoras en '{self.NOMBRE_FICHERO_RADIOS}'.",
|
||||
foreground='red').pack(pady=20)
|
||||
return
|
||||
# Lista completa de pestañas
|
||||
sub_tabs = ["Recursos", "Carrera", "Radios", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces", "Chat"]
|
||||
|
||||
frame_listado = ttk.Frame(frame_radio)
|
||||
frame_listado.pack(fill="both", expand=True)
|
||||
# Mapeo de la pestaña a la clase modular
|
||||
CLASES_MODULARES = {
|
||||
"Recursos": RecursosPanel,
|
||||
"Carrera": CarreraPanel,
|
||||
"Radios": RadioPanel,
|
||||
"Navegador": NavegadorPanel,
|
||||
"Correos": CorreosPanel,
|
||||
"Tareas": NotasPanel,
|
||||
"Alarmas": AlarmaPanel,
|
||||
"Enlaces": EnlacesPanel,
|
||||
"Chat": ChatPanel,
|
||||
}
|
||||
|
||||
listbox = tk.Listbox(frame_listado, height=15, width=60, font=('Arial', 10),
|
||||
bg=COLOR_BLANCO, fg=COLOR_TEXTO, selectbackground=COLOR_ACCION)
|
||||
listbox.pack(side="left", fill="both", expand=True, padx=5, pady=5)
|
||||
for sub_tab_text in sub_tabs:
|
||||
frame = ttk.Frame(sub_notebook, style='TFrame')
|
||||
sub_notebook.add(frame, text=sub_tab_text)
|
||||
self.tabs[sub_tab_text] = frame
|
||||
|
||||
scrollbar = ttk.Scrollbar(frame_listado, orient="vertical", command=listbox.yview)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
listbox.config(yscrollcommand=scrollbar.set)
|
||||
# Asegurar que el Frame de la pestaña se expande
|
||||
frame.grid_rowconfigure(0, weight=1)
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
for emisora in self.emisoras_cargadas:
|
||||
nombre_display = f"{emisora['nombre']} ({emisora.get('genero', 'N/D')})"
|
||||
listbox.insert(tk.END, nombre_display)
|
||||
ClasePanel = CLASES_MODULARES.get(sub_tab_text)
|
||||
vista_instancia = None
|
||||
|
||||
listbox.bind('<<ListboxSelect>>', lambda e: self.seleccionar_radio(listbox))
|
||||
if ClasePanel:
|
||||
# --- Lógica de Inicialización específica ---
|
||||
if sub_tab_text == "Recursos":
|
||||
vista_instancia = RecursosPanel(frame, self.figure, self.canvas)
|
||||
self.modulos[sub_tab_text] = vista_instancia
|
||||
|
||||
ttk.Label(frame_radio, text="URL del Stream:", font=FUENTE_NEGOCIOS).pack(pady=(10, 0), anchor="w")
|
||||
self.url_seleccionada_label = ttk.Label(frame_radio, text="N/A", wraplength=400, foreground=COLOR_TEXTO)
|
||||
self.url_seleccionada_label.pack(anchor="w")
|
||||
self.canvas = vista_instancia.canvas
|
||||
self.canvas_widget = vista_instancia.canvas_widget
|
||||
self.grafico_frame = vista_instancia.grafico_frame
|
||||
|
||||
def seleccionar_radio(self, listbox):
|
||||
"""Captura la selección y llama al reproductor para iniciar la reproducción."""
|
||||
seleccion = listbox.curselection()
|
||||
if seleccion:
|
||||
indice = seleccion[0]
|
||||
emisora = self.emisoras_cargadas[indice]
|
||||
url = emisora['url_stream']
|
||||
elif sub_tab_text == "Alarmas":
|
||||
vista_instancia = AlarmaPanel(frame, self.root, self.alarm_manager)
|
||||
self.modulos[sub_tab_text] = vista_instancia
|
||||
|
||||
self.radio_seleccionada.set(emisora['nombre'])
|
||||
self.url_seleccionada_label.config(text=url)
|
||||
else:
|
||||
vista_instancia = ClasePanel(frame, self.root)
|
||||
self.modulos[sub_tab_text] = vista_instancia
|
||||
|
||||
self.reproductor.cargar_y_reproducir(url)
|
||||
|
||||
def controlar_reproduccion(self, accion):
|
||||
"""Llama al método de control del reproductor (play/pause)."""
|
||||
if accion == 'play':
|
||||
self.reproductor.reproducir()
|
||||
elif accion == 'pause':
|
||||
self.reproductor.pausar()
|
||||
|
||||
def cambiar_volumen(self, valor):
|
||||
"""
|
||||
Ajusta el volumen, asegurando que el valor sea un entero para estabilizar el Scale.
|
||||
"""
|
||||
valor_entero = int(float(valor))
|
||||
|
||||
self.volumen_var.set(valor_entero)
|
||||
self.reproductor.ajustar_volumen(valor_entero)
|
||||
# Ubicar la instancia de la vista modular dentro de su Frame padre (CRÍTICO)
|
||||
vista_instancia.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🔔 LÓGICA Y VISTA DE T4 (ALARMAS / TEMPORIZADORES)
|
||||
# 📞 MÉTODOS DE CONEXIÓN (Llamados desde panel_lateral.py)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_alarmas(self, parent_frame):
|
||||
"""Crea la interfaz para programar y visualizar alarmas (H:M:S)."""
|
||||
def manejar_inicio_carrera(self):
|
||||
"""Inicia la carrera de camellos llamando al módulo de Carrera (T2)."""
|
||||
if 'Carrera' in self.modulos:
|
||||
self.modulos['Carrera'].manejar_inicio_carrera()
|
||||
else:
|
||||
messagebox.showerror("Error", "Módulo de Carrera no inicializado.")
|
||||
|
||||
frame = ttk.Frame(parent_frame, padding=10, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame, text="Programar Nuevo Temporizador (H:M:S)", font=FUENTE_NEGOCIOS).pack(pady=(0, 10))
|
||||
|
||||
# --- Controles de Nueva Alarma (H:M:S) ---
|
||||
frame_input = ttk.Frame(frame, style='TFrame')
|
||||
frame_input.pack(fill='x', pady=5)
|
||||
|
||||
# Horas
|
||||
ttk.Label(frame_input, text="Horas:").pack(side='left', padx=(0, 2))
|
||||
self.alarm_hours_entry = ttk.Entry(frame_input, width=3)
|
||||
self.alarm_hours_entry.pack(side='left', padx=(0, 10))
|
||||
self.alarm_hours_entry.insert(0, "0")
|
||||
|
||||
# Minutos
|
||||
ttk.Label(frame_input, text="Minutos:").pack(side='left', padx=(0, 2))
|
||||
self.alarm_minutes_entry = ttk.Entry(frame_input, width=3)
|
||||
self.alarm_minutes_entry.pack(side='left', padx=(0, 10))
|
||||
self.alarm_minutes_entry.insert(0, "1")
|
||||
|
||||
# Segundos
|
||||
ttk.Label(frame_input, text="Segundos:").pack(side='left', padx=(0, 2))
|
||||
self.alarm_seconds_entry = ttk.Entry(frame_input, width=3)
|
||||
self.alarm_seconds_entry.pack(side='left', padx=(0, 15))
|
||||
self.alarm_seconds_entry.insert(0, "0")
|
||||
|
||||
ttk.Button(frame_input, text="➕ Crear Alarma", command=self.manejar_nueva_alarma,
|
||||
style='Action.TButton').pack(side='left')
|
||||
|
||||
ttk.Separator(frame, orient='horizontal').pack(fill='x', pady=15)
|
||||
|
||||
# --- Listado de Alarmas Activas ---
|
||||
ttk.Label(frame, text="Alarmas Activas (Tiempo Restante)", font=FUENTE_NEGOCIOS).pack(pady=(0, 5))
|
||||
|
||||
self.alarm_list_frame = ttk.Frame(frame)
|
||||
self.alarm_list_frame.pack(fill="both", expand=True)
|
||||
|
||||
canvas = tk.Canvas(self.alarm_list_frame, borderwidth=0, background=COLOR_BLANCO)
|
||||
vscroll = ttk.Scrollbar(self.alarm_list_frame, orient="vertical", command=canvas.yview)
|
||||
|
||||
self.scrollable_frame = ttk.Frame(canvas)
|
||||
|
||||
self.scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: canvas.configure(
|
||||
scrollregion=canvas.bbox("all")
|
||||
)
|
||||
)
|
||||
|
||||
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=vscroll.set)
|
||||
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
vscroll.pack(side="right", fill="y")
|
||||
|
||||
def manejar_nueva_alarma(self):
|
||||
"""Captura los datos del formulario (H:M:S), los convierte a segundos y llama al AlarmManager."""
|
||||
try:
|
||||
# 1. Leer los valores (usando 'or 0' para manejar campos vacíos como 0)
|
||||
hours = int(self.alarm_hours_entry.get() or 0)
|
||||
minutes = int(self.alarm_minutes_entry.get() or 0)
|
||||
seconds = int(self.alarm_seconds_entry.get() or 0)
|
||||
|
||||
# 2. Calcular el total de segundos
|
||||
total_seconds = (hours * 3600) + (minutes * 60) + seconds
|
||||
|
||||
if total_seconds <= 0:
|
||||
print("⚠️ El tiempo de alarma debe ser un número positivo (H:M:S > 0).")
|
||||
return
|
||||
|
||||
# 3. Llamar al AlarmManager con el total de segundos
|
||||
self.alarm_manager.set_alarm(total_seconds)
|
||||
|
||||
# 4. Limpiar y preparar para la siguiente alarma (Default: 1 minuto)
|
||||
self.alarm_hours_entry.delete(0, tk.END)
|
||||
self.alarm_hours_entry.insert(0, "0")
|
||||
self.alarm_minutes_entry.delete(0, tk.END)
|
||||
self.alarm_minutes_entry.insert(0, "1")
|
||||
self.alarm_seconds_entry.delete(0, tk.END)
|
||||
self.alarm_seconds_entry.insert(0, "0")
|
||||
|
||||
self.actualizar_lista_alarmas()
|
||||
|
||||
except ValueError:
|
||||
print("⚠️ Por favor, introduce números enteros válidos para el tiempo.")
|
||||
except AttributeError:
|
||||
print("⚠️ Error: AlarmManager no inicializado.")
|
||||
|
||||
def manejar_cancelar_alarma(self, alarm_id):
|
||||
"""Cancela la alarma usando su ID."""
|
||||
if self.alarm_manager.cancel_alarm(alarm_id):
|
||||
self.actualizar_lista_alarmas()
|
||||
|
||||
def actualizar_lista_alarmas(self):
|
||||
"""Actualiza la visualización de las alarmas activas con botones de cancelación individuales."""
|
||||
if not self.scrollable_frame:
|
||||
self.after_alarm_id = self.after(1000, self.actualizar_lista_alarmas)
|
||||
return
|
||||
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
active_alarms = self.alarm_manager.get_active_alarms()
|
||||
|
||||
if not active_alarms:
|
||||
ttk.Label(self.scrollable_frame, text="--- No hay alarmas activas ---", font=('Consolas', 10),
|
||||
foreground=COLOR_TEXTO).pack(padx=10, pady=10)
|
||||
|
||||
for alarm in active_alarms:
|
||||
self.add_alarm_row(self.scrollable_frame, alarm)
|
||||
|
||||
self.after_alarm_id = self.after(1000, self.actualizar_lista_alarmas)
|
||||
|
||||
def add_alarm_row(self, parent, alarm_data):
|
||||
"""Añade una fila con la info de la alarma y su botón de cancelación."""
|
||||
row_frame = ttk.Frame(parent, padding=5, style='Note.TFrame')
|
||||
row_frame.pack(fill='x', padx=5, pady=2)
|
||||
|
||||
# Convertir total_seconds a formato Hh:Mm:Ss para la visualización del tiempo total
|
||||
total_s = alarm_data['total_seconds']
|
||||
h = total_s // 3600
|
||||
m = (total_s % 3600) // 60
|
||||
s = total_s % 60
|
||||
total_time_str = f"{h:02d}h:{m:02d}m:{s:02d}s"
|
||||
|
||||
# Info de la alarma
|
||||
info_text = (f"[ID{alarm_data['id']}] {alarm_data['restante']} -> {alarm_data['nombre']} "
|
||||
f"({total_time_str} total)")
|
||||
ttk.Label(row_frame, text=info_text, font=('Consolas', 10), style='Note.TLabel').pack(side='left', fill='x',
|
||||
expand=True)
|
||||
|
||||
# Botón de Cancelación Individual
|
||||
ttk.Button(row_frame, text="❌ Cancelar", style='Danger.TButton', width=10,
|
||||
command=lambda id=alarm_data['id']: self.manejar_cancelar_alarma(id)).pack(side='right')
|
||||
|
||||
def iniciar_actualizacion_alarmas(self):
|
||||
"""Inicia el ciclo de actualización de la lista de alarmas."""
|
||||
if self.alarm_manager:
|
||||
self.after_alarm_id = self.after(0, self.actualizar_lista_alarmas)
|
||||
|
||||
def detener_actualizacion_alarmas(self):
|
||||
"""Detiene el ciclo de actualización de la lista de alarmas."""
|
||||
if hasattr(self, 'after_alarm_id') and self.after_alarm_id:
|
||||
self.after_cancel(self.after_alarm_id)
|
||||
self.after_alarm_id = None
|
||||
print("Ciclo de actualización de alarmas detenido.")
|
||||
def cargar_contenido_web(self, titulo, contenido):
|
||||
"""Muestra el resultado del scraping llamando al módulo Navegador."""
|
||||
if 'Navegador' in self.modulos:
|
||||
self.modulos['Navegador'].cargar_contenido_web(titulo, contenido)
|
||||
else:
|
||||
messagebox.showerror("Error", "El módulo Navegador (Scrapping) no está inicializado.")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🔔 POPUP DE ALARMA (Notificación)
|
||||
# 🔔 POPUP DE ALARMA Y LÓGICA T1
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def show_alarm_popup(self, alarm_name, alarm_id):
|
||||
"""Muestra una ventana Toplevel sin barra de título ni botón de cierre."""
|
||||
|
||||
# 1. Crear la ventana popup
|
||||
"""Muestra una ventana emergente cuando una alarma salta."""
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title("🚨 ¡ALARMA!")
|
||||
|
||||
# --- Código de configuración del popup omitido por brevedad ---
|
||||
popup.geometry("350x150")
|
||||
popup.resizable(False, False)
|
||||
|
||||
# ✅ Eliminar la barra de título y los botones (incluido el de cierre 'X')
|
||||
popup.overrideredirect(True)
|
||||
|
||||
# Hacer que el popup sea modal (siempre encima)
|
||||
popup.transient(self.root)
|
||||
popup.grab_set()
|
||||
|
||||
# 2. Centrar la ventana
|
||||
self.root.update_idletasks()
|
||||
width = popup.winfo_width()
|
||||
height = popup.winfo_height()
|
||||
|
|
@ -382,15 +171,13 @@ class PanelCentral(ttk.Frame):
|
|||
y = (self.root.winfo_screenheight() // 2) - (height // 2)
|
||||
popup.geometry(f'{width}x{height}+{x}+{y}')
|
||||
|
||||
# 3. Función para cerrar y detener la música
|
||||
def close_and_stop(event=None):
|
||||
"""Función para cerrar el popup y detener el sonido."""
|
||||
self.alarm_manager.stop_alarm_sound()
|
||||
self.alarm_manager.cancel_alarm(alarm_id)
|
||||
popup.destroy()
|
||||
self.actualizar_lista_alarmas()
|
||||
if 'Alarmas' in self.modulos:
|
||||
self.modulos['Alarmas'].actualizar_lista_alarmas()
|
||||
|
||||
# 4. Contenido
|
||||
frame = ttk.Frame(popup, padding=20, relief='solid', borderwidth=2)
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
|
|
@ -399,319 +186,54 @@ class PanelCentral(ttk.Frame):
|
|||
ttk.Label(frame, text=f"Hora de Disparo: {alarm_name}", font=FUENTE_NEGOCIOS).pack(pady=5)
|
||||
ttk.Label(frame, text="Haz clic para cerrar.", font=('Arial', 9, 'italic'), foreground=COLOR_TEXTO).pack(pady=0)
|
||||
|
||||
# 5. Configurar el cierre
|
||||
# La forma de cerrar es mediante un clic en la ventana.
|
||||
popup.bind("<Button-1>", close_and_stop)
|
||||
frame.bind("<Button-1>", close_and_stop)
|
||||
|
||||
# 6. Esperar a que se cierre para continuar la ejecución del hilo principal de Tkinter
|
||||
self.root.wait_window(popup)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📄 LÓGICA Y VISTA DE TAREAS (Editor de Notas)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_tareas(self, parent_frame):
|
||||
"""Crea el editor de texto simple para el archivo res/notes dentro de la pestaña Tareas."""
|
||||
|
||||
frame = ttk.Frame(parent_frame, padding=15, style='TFrame')
|
||||
frame.pack(expand=True, fill="both")
|
||||
|
||||
ttk.Label(frame, text="Editor de Notas ", font=FUENTE_TITULO).pack(pady=(0, 10), anchor="w")
|
||||
ttk.Label(frame, text="Use este panel para tomar notas rápidas sobre la ejecución de tareas.",
|
||||
font=FUENTE_NEGOCIOS).pack(pady=(0, 15), anchor="w")
|
||||
|
||||
# 1. Widget de texto
|
||||
self.notes_text_editor = tk.Text(
|
||||
frame,
|
||||
height=20,
|
||||
wrap="word",
|
||||
bg=COLOR_BLANCO,
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
font=FUENTE_MONO
|
||||
)
|
||||
self.notes_text_editor.pack(fill="both", expand=True, pady=(0, 10))
|
||||
|
||||
# 2. Botones de Cargar y Guardar
|
||||
frame_botones = ttk.Frame(frame)
|
||||
frame_botones.pack(fill="x", pady=(5, 0))
|
||||
|
||||
ttk.Button(frame_botones, text="Guardar Cambios", command=self.guardar_res_notes, style='Action.TButton').pack(
|
||||
side=tk.RIGHT)
|
||||
ttk.Button(frame_botones, text="Cargar Archivo", command=self.cargar_res_notes, style='Action.TButton').pack(
|
||||
side=tk.LEFT)
|
||||
|
||||
self.cargar_res_notes(initial_load=True) # Carga inicial al crear la interfaz
|
||||
|
||||
def cargar_res_notes(self, initial_load=False):
|
||||
"""Carga el contenido de res/notes al editor de texto."""
|
||||
if not self.notes_text_editor: return
|
||||
|
||||
contenido = cargar_contenido_res_notes()
|
||||
|
||||
self.notes_text_editor.delete("1.0", tk.END)
|
||||
|
||||
if "Error al cargar:" in contenido:
|
||||
self.notes_text_editor.insert(tk.END, contenido)
|
||||
else:
|
||||
if initial_load and not contenido.strip():
|
||||
self.notes_text_editor.insert(tk.END, "# Escriba aquí sus notas (res/notes)")
|
||||
else:
|
||||
self.notes_text_editor.insert(tk.END, contenido)
|
||||
|
||||
if initial_load:
|
||||
print("Cargado 'res/notes' en la pestaña Tareas.")
|
||||
|
||||
def guardar_res_notes(self):
|
||||
"""Guarda el contenido del editor de texto en res/notes."""
|
||||
if not self.notes_text_editor: return
|
||||
|
||||
contenido = self.notes_text_editor.get("1.0", tk.END)
|
||||
|
||||
success, message = guardar_contenido_res_notes(contenido)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("✅ Guardado", "Notas guardadas exitosamente.")
|
||||
print(message)
|
||||
else:
|
||||
messagebox.showerror("❌ Error al Guardar", message)
|
||||
print(f"FALLO AL GUARDAR: {message}")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📦 ESTRUCTURA PRINCIPAL DEL PANEL
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_area_principal(self):
|
||||
"""Crea el contenedor de las subpestañas."""
|
||||
frame_izquierdo = ttk.Frame(self, style='TFrame')
|
||||
frame_izquierdo.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
frame_izquierdo.grid_rowconfigure(0, weight=1)
|
||||
frame_izquierdo.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.crear_notebook_pestañas(frame_izquierdo)
|
||||
|
||||
def crear_notebook_pestañas(self, parent_frame):
|
||||
"""Crea las pestañas internas para las tareas (T1, Carrera, Radios, Tareas, Alarmas, etc.)."""
|
||||
sub_notebook = ttk.Notebook(parent_frame)
|
||||
sub_notebook.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
sub_tabs = ["Recursos", "Carrera", "Radios", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"]
|
||||
self.tabs = {}
|
||||
|
||||
for i, sub_tab_text in enumerate(sub_tabs):
|
||||
frame = ttk.Frame(sub_notebook, style='TFrame')
|
||||
sub_notebook.add(frame, text=sub_tab_text)
|
||||
self.tabs[sub_tab_text] = frame
|
||||
|
||||
if sub_tab_text == "Recursos":
|
||||
self.grafico_frame = ttk.Frame(frame, style='TFrame')
|
||||
self.grafico_frame.pack(expand=True, fill="both", padx=10, pady=10)
|
||||
|
||||
self.canvas = FigureCanvasTkAgg(self.figure, master=self.grafico_frame)
|
||||
self.canvas_widget = self.canvas.get_tk_widget()
|
||||
self.canvas_widget.pack(expand=True, fill="both")
|
||||
|
||||
elif sub_tab_text == "Carrera":
|
||||
self.crear_interfaz_carrera(frame)
|
||||
|
||||
elif sub_tab_text == "Radios":
|
||||
self.crear_interfaz_radios(frame)
|
||||
|
||||
elif sub_tab_text == "Tareas":
|
||||
self.crear_interfaz_tareas(frame) # <--- Llamada a la nueva interfaz
|
||||
|
||||
elif sub_tab_text == "Alarmas":
|
||||
self.crear_interfaz_alarmas(frame)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
||||
# 🐪 LÓGICA DE T2 (CARRERA DE CAMELLOS)
|
||||
# ... (El código de carreraCamellos no cambia)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_carrera(self, parent_frame):
|
||||
"""Crea los controles y la visualización de la Carrera de Camellos."""
|
||||
frame_controles = ttk.Frame(parent_frame, style='TFrame', padding=10)
|
||||
frame_controles.pack(fill="x")
|
||||
self.frame_carrera_controles = frame_controles
|
||||
|
||||
ttk.Label(frame_controles, text="Resultado de Carrera de Camellos (T2 Sincronización)",
|
||||
style='TLabel', font=FUENTE_NEGOCIOS).pack(side="left", padx=5)
|
||||
|
||||
self.carrera_estado_label = ttk.Label(frame_controles, text="Estado.", style='TLabel', font=FUENTE_NEGOCIOS)
|
||||
self.carrera_estado_label.pack(side="right", padx=10)
|
||||
|
||||
self.frame_progreso = ttk.Frame(parent_frame, style='TFrame', padding=10)
|
||||
self.frame_progreso.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(self.frame_progreso,
|
||||
text="Presiona el botón 'App2 (T2-Carrera 🏁)' en el panel lateral para iniciar la simulación de hilos.",
|
||||
style='TLabel').pack(pady=20)
|
||||
|
||||
def manejar_inicio_carrera(self):
|
||||
"""Inicia una nueva carrera de camellos con un número aleatorio de participantes."""
|
||||
if self.camellos and any(c.is_alive() for c in self.camellos):
|
||||
self.carrera_estado_label.config(text="⚠️ Ya hay una carrera en curso.")
|
||||
return
|
||||
|
||||
print("Iniciando Carrera de Camellos con número variable de participantes...")
|
||||
|
||||
num_camellos = random.randint(10, 20)
|
||||
nombres = [f"Camello {i + 1}" for i in range(num_camellos)]
|
||||
|
||||
for widget in self.frame_progreso.winfo_children():
|
||||
widget.destroy()
|
||||
self.progreso_labels = {}
|
||||
|
||||
self.camellos = iniciar_carrera(nombres)
|
||||
|
||||
self.carrera_estado_label.config(text=f"¡Carrera en marcha! 💨 ({num_camellos} participantes)")
|
||||
|
||||
self.crear_visualizacion_carrera(nombres)
|
||||
self.iniciar_actualizacion_carrera()
|
||||
|
||||
def crear_visualizacion_carrera(self, nombres):
|
||||
"""Prepara el layout de la carrera."""
|
||||
for widget in self.frame_progreso.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
for i, nombre in enumerate(nombres):
|
||||
ttk.Label(self.frame_progreso, text=f"{nombre}: ", style='TLabel', font=FUENTE_NEGOCIOS).grid(row=i,
|
||||
column=0,
|
||||
sticky="w")
|
||||
|
||||
label_progreso = ttk.Label(self.frame_progreso, text="[Esperando...]", style='TLabel',
|
||||
foreground=COLOR_TEXTO)
|
||||
label_progreso.grid(row=i, column=1, sticky="w", padx=10)
|
||||
self.progreso_labels[nombre] = label_progreso
|
||||
|
||||
label_posicion = ttk.Label(self.frame_progreso, text="", style='TLabel')
|
||||
label_posicion.grid(row=i, column=2, sticky="w")
|
||||
self.progreso_labels[f'{nombre}_pos'] = label_posicion
|
||||
|
||||
self.carrera_info_label = ttk.Label(self.frame_progreso, text="", style='TLabel', font=FUENTE_NEGOCIOS)
|
||||
self.carrera_info_label.grid(row=len(nombres), column=0, columnspan=3, sticky="w", pady=(10, 0))
|
||||
|
||||
def mostrar_progreso_activo(self, datos_activos):
|
||||
"""Actualiza la visualización de la carrera mientras los hilos están corriendo."""
|
||||
|
||||
if not datos_activos['camellos']:
|
||||
return
|
||||
|
||||
nombres_activos = [c['nombre'] for c in datos_activos['camellos']]
|
||||
if not self.progreso_labels or any(n not in self.progreso_labels for n in nombres_activos):
|
||||
self.crear_visualizacion_carrera(nombres_activos)
|
||||
|
||||
for estado in datos_activos['camellos']:
|
||||
nombre = estado['nombre']
|
||||
progreso = estado['progreso']
|
||||
etiqueta_progreso = self.progreso_labels.get(nombre)
|
||||
etiqueta_posicion = self.progreso_labels.get(f'{nombre}_pos')
|
||||
|
||||
if etiqueta_progreso:
|
||||
barra = "█" * (progreso // 2)
|
||||
texto_progreso = f"[{barra}{' ' * (25 - len(barra))}] ({progreso}/50) Estado: {estado['estado']}"
|
||||
etiqueta_progreso.config(text=texto_progreso)
|
||||
|
||||
if etiqueta_posicion and estado['posicion']:
|
||||
etiqueta_posicion.config(text=f"POS: {estado['posicion']} 🏆", foreground=COLOR_ACCION)
|
||||
|
||||
self.carrera_estado_label.config(text="Carrera en curso...")
|
||||
self.carrera_info_label.config(text="")
|
||||
|
||||
def mostrar_resultado_final(self, resultado_final):
|
||||
"""Muestra el resultado final persistente de la carrera."""
|
||||
|
||||
nombres_finales = [c['nombre'] for c in resultado_final['camellos']]
|
||||
if not self.progreso_labels or any(n not in self.progreso_labels for n in nombres_finales):
|
||||
self.crear_visualizacion_carrera(nombres_finales)
|
||||
|
||||
camellos_ordenados = sorted(resultado_final['camellos'], key=lambda x: x['posicion'])
|
||||
|
||||
for estado in camellos_ordenados:
|
||||
nombre = estado['nombre']
|
||||
etiqueta_progreso = self.progreso_labels.get(nombre)
|
||||
etiqueta_posicion = self.progreso_labels.get(f'{nombre}_pos')
|
||||
|
||||
if etiqueta_progreso:
|
||||
barra = "█" * (estado['progreso'] // 2)
|
||||
texto_progreso = f"[{barra}{' ' * (25 - len(barra))}] (50/50) Estado: Meta"
|
||||
etiqueta_progreso.config(text=texto_progreso)
|
||||
|
||||
if etiqueta_posicion:
|
||||
etiqueta_posicion.config(text=f"POS: {estado['posicion']} {'🏆' if estado['posicion'] == 1 else ''}",
|
||||
foreground=COLOR_ACCION)
|
||||
|
||||
self.carrera_estado_label.config(text="✅ Carrera Terminada.")
|
||||
self.carrera_info_label.config(text=f"¡El ganador es: {resultado_final['ganador']}!",
|
||||
font=FUENTE_TITULO, foreground=COLOR_EXITO)
|
||||
|
||||
def actualizar_carrera(self):
|
||||
"""Ciclo de actualización visual: lee el estado activo o el resultado final persistente."""
|
||||
|
||||
estado = obtener_estado_carrera(self.camellos)
|
||||
|
||||
if estado['tipo'] == 'final':
|
||||
self.mostrar_resultado_final(estado['datos'])
|
||||
self.detener_actualizacion_carrera()
|
||||
return
|
||||
|
||||
elif estado['tipo'] == 'activo':
|
||||
self.mostrar_progreso_activo(estado['datos'])
|
||||
|
||||
self.after_carrera_id = self.after(self.INTERVALO_CARRERA_MS, self.actualizar_carrera)
|
||||
|
||||
def iniciar_actualizacion_carrera(self):
|
||||
"""Inicia el ciclo de actualización visual (o carga el resultado guardado al inicio)."""
|
||||
self.detener_actualizacion_carrera()
|
||||
|
||||
if RESULTADO_ULTIMO and not RESULTADO_ULTIMO.get('activa', True) and self.frame_progreso:
|
||||
self.mostrar_resultado_final(RESULTADO_ULTIMO)
|
||||
else:
|
||||
self.carrera_estado_label.config(text="Carrera lista para empezar.")
|
||||
self.after_carrera_id = self.after(0, self.actualizar_carrera)
|
||||
|
||||
def detener_actualizacion_carrera(self):
|
||||
"""Detiene el ciclo de actualización visual de la carrera."""
|
||||
if self.after_carrera_id:
|
||||
self.after_cancel(self.after_carrera_id)
|
||||
self.after_carrera_id = None
|
||||
print("Ciclo de actualización de carrera detenido.")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 📈 LÓGICA DE T1 (MONITOR DE RECURSOS)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def actualizar_recursos(self):
|
||||
"""Obtiene los datos del sistema (incluyendo Red) y dibuja/redibuja el gráfico."""
|
||||
"""Obtiene los datos del sistema y delega el dibujo al módulo RecursosPanel."""
|
||||
try:
|
||||
net_in, net_out = self.net_monitor.get_io_data_kb()
|
||||
actualizar_historial_datos(net_in, net_out)
|
||||
if self.net_monitor is None:
|
||||
raise Exception("TrafficMeter no inicializado.")
|
||||
|
||||
if self.canvas:
|
||||
crear_grafico_recursos(self.figure)
|
||||
self.canvas.draw()
|
||||
net_in, net_out, cpu_percent, ram_percent = self.net_monitor.get_io_data_kb()
|
||||
|
||||
if 'Recursos' in self.modulos:
|
||||
self.modulos['Recursos'].actualizar_datos(net_in, net_out, cpu_percent, ram_percent)
|
||||
self.modulos['Recursos'].dibujar_grafico()
|
||||
|
||||
if self.canvas:
|
||||
self.canvas.draw()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error al generar el gráfico de recursos: {e}"
|
||||
print(error_msg)
|
||||
if self.canvas_widget and self.canvas_widget.winfo_exists():
|
||||
self.canvas_widget.pack_forget()
|
||||
error_label = ttk.Label(self.grafico_frame, text=error_msg, foreground='red', style='TLabel')
|
||||
error_label.pack(pady=20)
|
||||
self.detener_actualizacion_automatica()
|
||||
print(f"Error en la actualización de recursos T1: {e}")
|
||||
pass
|
||||
|
||||
self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos)
|
||||
|
||||
def iniciar_actualizacion_automatica(self):
|
||||
"""Inicia el ciclo de actualización del gráfico de recursos."""
|
||||
"""
|
||||
Inicia el ciclo de actualización del gráfico de recursos.
|
||||
Inicializa TrafficMeter si no se hizo en __init__.
|
||||
"""
|
||||
if self.net_monitor is None:
|
||||
print("Inicializando TrafficMeter antes de actualizar recursos.")
|
||||
try:
|
||||
self.net_monitor = iniciar_monitor_red()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error de Hilo", f"No se pudo iniciar TrafficMeter. ¿Falta 'psutil'? Detalle: {e}")
|
||||
print("No se pudo iniciar TrafficMeter. La actualización automática no comenzará.")
|
||||
return
|
||||
|
||||
print("Iniciando actualización automática de recursos.")
|
||||
self.after_id = self.after(0, self.actualizar_recursos)
|
||||
|
||||
def detener_actualizacion_automatica(self):
|
||||
"""Detiene el ciclo de actualización periódica y los hilos/tareas."""
|
||||
"""Detiene el ciclo de actualización periódica y los hilos/tareas de los módulos."""
|
||||
if self.after_id:
|
||||
self.after_cancel(self.after_id)
|
||||
self.after_id = None
|
||||
|
|
@ -722,77 +244,8 @@ class PanelCentral(ttk.Frame):
|
|||
self.net_monitor.join()
|
||||
print("Hilo de TrafficMeter detenido.")
|
||||
|
||||
self.detener_actualizacion_carrera()
|
||||
self.detener_actualizacion_alarmas()
|
||||
# Llama a los métodos de detención de los módulos (si existen)
|
||||
for nombre, modulo in self.modulos.items():
|
||||
if hasattr(modulo, 'detener_actualizacion'):
|
||||
modulo.detener_actualizacion()
|
||||
|
||||
if self.reproductor:
|
||||
self.reproductor.detener()
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 💬 PANEL CHAT, ALUMNOS Y APPS (PANEL LATERAL)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_panel_chat_y_alumnos(self, ):
|
||||
"""Crea el panel de chat y lista de Alumnos (columna derecha), incluyendo el reproductor."""
|
||||
panel_chat = ttk.Frame(self, style='TFrame', padding="10")
|
||||
panel_chat.grid(row=0, column=1, sticky="nsew")
|
||||
|
||||
panel_chat.grid_rowconfigure(5, weight=1)
|
||||
panel_chat.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# --- FILA 0: Título ---
|
||||
ttk.Label(panel_chat, text="Chat", foreground=COLOR_ACCION, font=FUENTE_TITULO, style='TLabel').grid(row=0,
|
||||
column=0,
|
||||
pady=(0,
|
||||
10),
|
||||
sticky="w")
|
||||
|
||||
# --- FILAS 1-3: Chat Input ---
|
||||
ttk.Label(panel_chat, text="Mensaje", style='TLabel').grid(row=1, column=0, sticky="w")
|
||||
chat_text = tk.Text(panel_chat, height=6, width=30, bg=COLOR_BLANCO, relief="solid", borderwidth=1,
|
||||
font=('Arial', 10))
|
||||
chat_text.grid(row=2, column=0, sticky="ew", pady=(0, 5))
|
||||
ttk.Button(panel_chat, text="Enviar", style='Action.TButton').grid(row=3, column=0, pady=(0, 15), sticky="e")
|
||||
|
||||
# --- FILAS 4-7: Alumnos (Se expanden) ---
|
||||
for i in range(1, 4):
|
||||
frame_alumno = ttk.Frame(panel_chat, style='Alumno.TFrame', padding=8)
|
||||
frame_alumno.grid(row=3 + i, column=0, sticky="ew", pady=5)
|
||||
frame_alumno.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(frame_alumno, text=f"Alumno {i}", font=('Arial', 11, 'bold'), style='Alumno.TLabel').grid(row=0,
|
||||
column=0,
|
||||
sticky="w")
|
||||
ttk.Label(frame_alumno, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit.", wraplength=250,
|
||||
justify=tk.LEFT, style='Alumno.TLabel').grid(row=1, column=0, sticky="w")
|
||||
ttk.Button(frame_alumno, text="↻", width=3, style='Action.TButton').grid(row=0, column=1, rowspan=2, padx=5,
|
||||
sticky="ne")
|
||||
|
||||
# --- FILA 8: Reproductor Música (T3) ---
|
||||
musica_frame = ttk.LabelFrame(panel_chat, text="Reproductor Música", padding=10, style='TFrame')
|
||||
musica_frame.grid(row=8, column=0, sticky="ew", pady=(15, 0))
|
||||
|
||||
# 8a: Emisora Actual
|
||||
ttk.Label(musica_frame, text="Actual: ", style='TLabel').grid(row=0, column=0, sticky="w")
|
||||
ttk.Label(musica_frame, textvariable=self.radio_seleccionada, font=('Arial', 10, 'bold'), style='TLabel').grid(
|
||||
row=0, column=1, columnspan=2, sticky="w")
|
||||
|
||||
# 8b: Controles de Reproducción (Play/Pause)
|
||||
frame_controles_musica = ttk.Frame(musica_frame, style='TFrame')
|
||||
frame_controles_musica.grid(row=1, column=0, columnspan=3, pady=(5, 5))
|
||||
|
||||
ttk.Button(frame_controles_musica, text="▶️ Iniciar", command=lambda: self.controlar_reproduccion('play'),
|
||||
style='Action.TButton', width=8).pack(side="left", padx=5)
|
||||
ttk.Button(frame_controles_musica, text="⏸️ Pausar", command=lambda: self.controlar_reproduccion('pause'),
|
||||
style='Action.TButton', width=8).pack(side="left", padx=5)
|
||||
|
||||
# 8c: Control de Volumen (Scale/Deslizable)
|
||||
ttk.Label(musica_frame, text="Volumen:", style='TLabel').grid(row=2, column=0, sticky="w")
|
||||
|
||||
volumen_scale = ttk.Scale(musica_frame, from_=0, to=100, orient="horizontal",
|
||||
variable=self.volumen_var, command=self.cambiar_volumen)
|
||||
volumen_scale.grid(row=2, column=1, sticky="ew", padx=(0, 5))
|
||||
|
||||
ttk.Label(musica_frame, textvariable=self.volumen_var, style='TLabel').grid(row=2, column=2, sticky="w")
|
||||
|
||||
musica_frame.grid_columnconfigure(1, weight=1)
|
||||
|
|
@ -3,43 +3,81 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
|
||||
# --- Módulos de Lógica Existente ---
|
||||
# Asumiendo que estos módulos existen en la estructura lógica del proyecto
|
||||
from logica.controlador import accion_placeholder
|
||||
from logica.T1.backup import accion_backup_t1
|
||||
from logica.T1.runVScode import abrir_vscode
|
||||
from logica.T1.openBrowser import navegar_a_url
|
||||
from logica.T2.scraping import hacer_scraping # <--- NUEVA IMPORTACIÓN DE SCRAPING
|
||||
from logica.T2.scraping import hacer_scraping
|
||||
|
||||
# --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py ---
|
||||
# Asumo que este archivo existe y contiene las constantes de color/fuente
|
||||
# --- Módulos de Vistas ---
|
||||
# Importamos la clase RadioPanel, que contiene los controles de música (Play/Pause y Volumen).
|
||||
from vista.central_panel.view_radio import RadioPanel
|
||||
from vista.config import *
|
||||
|
||||
class PanelLateral(ttk.Frame):
|
||||
"""Contiene el menú de botones y entradas para las tareas."""
|
||||
|
||||
class PanelLateral(ttk.Frame):
|
||||
"""
|
||||
Panel lateral izquierdo de la aplicación.
|
||||
Contiene la barra de entrada, botones de lógica (Extracción, Navegación, Backup)
|
||||
y los controles de música esenciales.
|
||||
"""
|
||||
|
||||
# Usamos la constante definida en vista/config.py
|
||||
ANCHO_CARACTERES_FIJO = ANCHO_CARACTERES_PANEL_LATERAL
|
||||
|
||||
def __init__(self, parent, central_panel=None, *args, **kwargs):
|
||||
def __init__(self, parent, root, panel_central, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
self.central_panel = central_panel
|
||||
self.root = root
|
||||
self.panel_central = panel_central
|
||||
self.controles_musica = None
|
||||
|
||||
self.configurar_estilos_locales(parent)
|
||||
self.entrada_superior = None
|
||||
|
||||
# 1. Entrada superior (barra de entrada)
|
||||
self.entrada_superior = ttk.Entry(self, width=self.ANCHO_CARACTERES_FIJO, style='Yellow.TEntry')
|
||||
self.entrada_superior.pack(fill="x", pady=10, padx=5, ipady=3)
|
||||
self.configurar_estilos_locales(root)
|
||||
|
||||
# Configuración de Layout Principal
|
||||
self.grid_rowconfigure(99, weight=1) # Permite que la Fila 99 se expanda
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# Creación de Secciones
|
||||
self.crear_barra_de_entrada() # Fila 0
|
||||
self.crear_seccion_acciones() # Fila 1
|
||||
self.crear_seccion_aplicaciones() # Fila 2
|
||||
self.crear_seccion_batch() # Fila 3
|
||||
|
||||
# Separador y Espacio Expandible
|
||||
ttk.Separator(self, orient='horizontal').grid(row=4, column=0, sticky="ew", pady=(10, 0))
|
||||
tk.Frame(self, height=1).grid(row=99, column=0, sticky="nsew")
|
||||
|
||||
self.crear_controles_musica() # Fila 100
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🖼️ ESTRUCTURA Y WIDGETS
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_barra_de_entrada(self):
|
||||
"""Crea la entrada superior para URL/Scraping."""
|
||||
frame_entrada = ttk.Frame(self, style='TFrame', padding="10 5 10 0")
|
||||
frame_entrada.grid(row=0, column=0, sticky="ew")
|
||||
|
||||
self.entrada_superior = ttk.Entry(frame_entrada, width=self.ANCHO_CARACTERES_FIJO, style='Yellow.TEntry')
|
||||
self.entrada_superior.pack(fill="x", ipady=3)
|
||||
self.entrada_superior.bind('<Return>', self.manejar_navegacion)
|
||||
|
||||
# 2. Área de Extracción/Navegación
|
||||
def crear_seccion_acciones(self):
|
||||
"""Crea los botones de Extracción/Navegación."""
|
||||
acciones_extraccion = [
|
||||
# CAMBIO: Botón renombrado y vinculado a la nueva lógica de scraping
|
||||
("Extraer Datos", self.manejar_extraccion_datos),
|
||||
("Extraer Datos (Wikipedia)", self.manejar_extraccion_datos),
|
||||
("Ir a la URL usando el navegador", self.manejar_navegacion),
|
||||
# El botón de Google se mantiene como placeholder, esperando la implementación
|
||||
("Buscar API Google", lambda: accion_placeholder("Buscar API Google"))
|
||||
]
|
||||
self.crear_seccion(self, titulo="", acciones=acciones_extraccion)
|
||||
self._crear_bloque_botones(self, titulo="Extracción/Navegación", acciones=acciones_extraccion, grid_row=1)
|
||||
|
||||
# 3. Área de Aplicaciones
|
||||
def crear_seccion_aplicaciones(self):
|
||||
"""Crea los botones de apertura de aplicaciones."""
|
||||
app2_comando = self.manejar_inicio_carrera_t2
|
||||
|
||||
acciones_aplicaciones = [
|
||||
|
|
@ -47,25 +85,36 @@ class PanelLateral(ttk.Frame):
|
|||
("App2 (Carrera 🏁)", app2_comando),
|
||||
("App3", lambda: accion_placeholder("App3"))
|
||||
]
|
||||
self.crear_seccion(self, titulo="Aplicaciones", acciones=acciones_aplicaciones)
|
||||
self._crear_bloque_botones(self, titulo="Aplicaciones", acciones=acciones_aplicaciones, grid_row=2)
|
||||
|
||||
# 4. Área de Procesos Batch
|
||||
def crear_seccion_batch(self):
|
||||
"""Crea el botón de Copias de seguridad."""
|
||||
acciones_batch = [
|
||||
("Copias de seguridad", self.manejar_backup)
|
||||
]
|
||||
self.crear_seccion(self, titulo="Procesos batch", acciones=acciones_batch)
|
||||
self._crear_bloque_botones(self, titulo="Procesos batch", acciones=acciones_batch, grid_row=3)
|
||||
|
||||
# 5. Espacio expandible
|
||||
tk.Frame(self, height=1).pack(expand=True, fill="both")
|
||||
def crear_controles_musica(self):
|
||||
"""Crea el área para alojar los controles de música/radio."""
|
||||
frame_musica = ttk.Frame(self, style='TFrame', padding="15 10")
|
||||
frame_musica.grid(row=100, column=0, sticky="ew")
|
||||
frame_musica.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# --- MÉTODOS DE LÓGICA / CONTROL ---
|
||||
# Instancia la clase RadioPanel, que ahora contiene solo Play/Pause y Volumen
|
||||
self.controles_musica = RadioPanel(frame_musica, self.root)
|
||||
self.controles_musica.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
frame_musica.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# ⏯️ MÉTODOS DE LÓGICA / CONTROL
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def manejar_extraccion_datos(self):
|
||||
"""
|
||||
Obtiene el término de búsqueda de la entrada superior, realiza el scraping
|
||||
en Wikipedia (URL base fija), muestra el resultado y lo carga en el Panel Central.
|
||||
Obtiene el término de búsqueda, realiza el scraping, y carga el resultado
|
||||
en el módulo Navegador del Panel Central.
|
||||
"""
|
||||
# Obtenemos el texto introducido por el usuario (el término de búsqueda)
|
||||
termino_busqueda = self.entrada_superior.get().strip()
|
||||
|
||||
if not termino_busqueda:
|
||||
|
|
@ -73,15 +122,13 @@ class PanelLateral(ttk.Frame):
|
|||
"Por favor, introduce un término de búsqueda para extraer datos.")
|
||||
return
|
||||
|
||||
# Llama a la lógica de scraping. Se esperan 3 valores: éxito, mensaje y contenido.
|
||||
success, message, contenido = hacer_scraping(termino_busqueda)
|
||||
|
||||
if success:
|
||||
messagebox.showinfo("✅ Extracción Exitosa", message)
|
||||
|
||||
# Llamada al Panel Central para visualizar el resultado del scraping
|
||||
if self.central_panel:
|
||||
self.central_panel.cargar_texto_en_tareas(termino_busqueda, contenido)
|
||||
if self.panel_central:
|
||||
self.panel_central.cargar_contenido_web(f"Scraping: {termino_busqueda}", contenido)
|
||||
else:
|
||||
messagebox.showerror("Error", "No se puede visualizar el resultado: Panel Central no disponible.")
|
||||
|
||||
|
|
@ -92,21 +139,15 @@ class PanelLateral(ttk.Frame):
|
|||
"""
|
||||
Llama al método 'manejar_inicio_carrera' del Panel Central.
|
||||
"""
|
||||
if self.central_panel:
|
||||
if self.panel_central:
|
||||
print("Botón App2 presionado. Iniciando Carrera de Camellos en Panel Central...")
|
||||
self.central_panel.manejar_inicio_carrera()
|
||||
|
||||
# Opcional: Cambiar automáticamente a la pestaña Resultados
|
||||
if "Carrera" in self.central_panel.tabs:
|
||||
notebook = self.central_panel.tabs["Carrera"].winfo_toplevel().winfo_children()[0]
|
||||
if isinstance(notebook, ttk.Notebook):
|
||||
notebook.select(self.central_panel.tabs["Carrera"])
|
||||
self.panel_central.manejar_inicio_carrera()
|
||||
else:
|
||||
messagebox.showerror("Error", "El Panel Central no está inicializado.")
|
||||
|
||||
def manejar_navegacion(self, event=None):
|
||||
"""
|
||||
Obtiene el texto de la entrada superior y llama a la función de navegación.
|
||||
Obtiene el texto de la entrada superior y llama a la función de navegación (abrir navegador externo).
|
||||
"""
|
||||
url = self.entrada_superior.get()
|
||||
if navegar_a_url(url):
|
||||
|
|
@ -122,6 +163,10 @@ class PanelLateral(ttk.Frame):
|
|||
else:
|
||||
messagebox.showerror("❌ Error en el Backup", message)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# ⚙️ MÉTODOS HELPER
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def configurar_estilos_locales(self, parent):
|
||||
"""Configura estilos para los widgets del panel lateral, usando constantes importadas."""
|
||||
style = ttk.Style(parent)
|
||||
|
|
@ -131,8 +176,7 @@ class PanelLateral(ttk.Frame):
|
|||
|
||||
style.configure('Green.TButton', background=COLOR_EXITO, foreground=COLOR_BLANCO, font=FUENTE_NEGOCIOS,
|
||||
relief='flat', padding=[10, 5])
|
||||
style.map('Green.TButton', background=[('active', '#388E3C'), ('pressed',
|
||||
'#1B5E20')])
|
||||
style.map('Green.TButton', background=[('active', '#388E3C'), ('pressed', '#1B5E20')])
|
||||
|
||||
style.configure('Action.TButton', background=COLOR_ACCION, foreground=COLOR_BLANCO, font=FUENTE_NEGOCIOS,
|
||||
relief='flat', padding=[10, 5])
|
||||
|
|
@ -143,14 +187,16 @@ class PanelLateral(ttk.Frame):
|
|||
relief='flat', padding=[5, 3])
|
||||
style.map('SmallAction.TButton', background=[('active', COLOR_ACCION_HOVER), ('pressed', COLOR_ACCION_PRESSED)])
|
||||
|
||||
def crear_seccion(self, parent_frame, titulo, acciones):
|
||||
"""Función helper para crear secciones de etiquetas y botones."""
|
||||
if titulo:
|
||||
ttk.Label(parent_frame, text=titulo, font=FUENTE_NEGOCIOS).pack(fill="x", pady=(10, 0), padx=5)
|
||||
def _crear_bloque_botones(self, parent_frame, titulo, acciones, grid_row):
|
||||
"""Función helper para crear secciones de etiquetas y botones usando GRID."""
|
||||
|
||||
frame_botones = ttk.Frame(parent_frame, style='TFrame')
|
||||
frame_botones.pack(fill="x", pady=5, padx=5)
|
||||
frame_seccion = ttk.Frame(parent_frame, style='TFrame', padding="10 0 10 5")
|
||||
frame_seccion.grid(row=grid_row, column=0, sticky="ew")
|
||||
|
||||
if titulo:
|
||||
frame_titulo = ttk.Frame(frame_seccion, style='TFrame')
|
||||
frame_titulo.pack(fill="x", pady=(10, 0))
|
||||
ttk.Label(frame_titulo, text=titulo, font=FUENTE_NEGOCIOS).pack(anchor="w", padx=5)
|
||||
|
||||
for texto_boton, comando in acciones:
|
||||
# Usamos el estilo 'Green.TButton' para los botones de acción principal
|
||||
ttk.Button(frame_botones, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5)
|
||||
ttk.Button(frame_seccion, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
# Módulo: ventana_principal.py
|
||||
# Módulo: vista/ventana_principal.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from vista.panel_lateral import PanelLateral
|
||||
from vista.panel_central import PanelCentral
|
||||
# Asegúrate de que estas funciones de lógica existen y funcionan sin error.
|
||||
from logica.T2.getLocalTime import obtener_hora_local
|
||||
from logica.T2.getWeather import obtener_datos_clima
|
||||
|
||||
|
|
@ -23,31 +24,32 @@ class VentanaPrincipal(tk.Tk):
|
|||
self.reloj_after_id = None
|
||||
self.label_clima = None
|
||||
self.clima_after_id = None
|
||||
self.panel_central = None # Inicializar a None para evitar errores en on_closing
|
||||
|
||||
style = ttk.Style()
|
||||
style.theme_use('clam')
|
||||
|
||||
# Usando la constante importada
|
||||
self.config(bg=COLOR_FONDO)
|
||||
self.configurar_estilos(style)
|
||||
|
||||
# Configuración del protocolo de cierre
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
|
||||
# Configuración de la cuadrícula
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(1, weight=0)
|
||||
self.grid_columnconfigure(0, weight=0)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
self.grid_rowconfigure(1, weight=0) # Fila de la barra inferior
|
||||
self.grid_columnconfigure(0, weight=0) # Columna del panel lateral (ancho fijo)
|
||||
self.grid_columnconfigure(1, weight=1) # Columna del panel central
|
||||
|
||||
self.crear_paneles_principales()
|
||||
self.crear_barra_inferior()
|
||||
|
||||
# Inicio de los ciclos de actualización
|
||||
self.iniciar_actualizacion_reloj()
|
||||
self.iniciar_actualizacion_clima()
|
||||
|
||||
def configurar_estilos(self, s: ttk.Style):
|
||||
"""Define estilos visuales personalizados sin dependencias externas usando constantes de config."""
|
||||
|
||||
# Los colores se importan, ya no se definen aquí
|
||||
"""Define estilos visuales personalizados."""
|
||||
|
||||
s.configure('TFrame', background=COLOR_FONDO)
|
||||
s.configure('TLabel', background=COLOR_FONDO, foreground=COLOR_TEXTO, font=FUENTE_NORMAL)
|
||||
|
|
@ -55,11 +57,10 @@ class VentanaPrincipal(tk.Tk):
|
|||
s.configure('Action.TButton',
|
||||
background=COLOR_ACCION,
|
||||
foreground=COLOR_BLANCO,
|
||||
font=FUENTE_NEGOCIOS, # Usando FUENTE_NEGOCIOS
|
||||
font=FUENTE_NEGOCIOS,
|
||||
relief='flat',
|
||||
padding=[10, 5])
|
||||
|
||||
# Mapeo de colores de estado de botón (usando las constantes de hover/pressed)
|
||||
s.map('Action.TButton',
|
||||
background=[('active', COLOR_ACCION_HOVER),
|
||||
('pressed', COLOR_ACCION_PRESSED)])
|
||||
|
|
@ -77,20 +78,23 @@ class VentanaPrincipal(tk.Tk):
|
|||
s.map('TNotebook.Tab', background=[('selected', COLOR_FONDO)], foreground=[('selected', COLOR_ACCION)])
|
||||
|
||||
def crear_paneles_principales(self):
|
||||
"""Ensambla el panel lateral y el panel central en la rejilla, asegurando el ancho del lateral."""
|
||||
"""Ensambla el panel lateral y el panel central."""
|
||||
|
||||
# Panel Central debe inicializarse primero para pasar la referencia al lateral
|
||||
# self es parent
|
||||
self.panel_central = PanelCentral(self, self)
|
||||
self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10)
|
||||
|
||||
# Usando la constante importada
|
||||
self.panel_lateral = PanelLateral(self, central_panel=self.panel_central, width=ANCHO_PANEL_LATERAL)
|
||||
# 🎯 CORRECCIÓN CLAVE: Pasar 'self' como argumento 'root' y 'self.panel_central' como argumento posicional.
|
||||
# PanelLateral espera: PanelLateral(parent, root, panel_central, ...)
|
||||
self.panel_lateral = PanelLateral(self, self, self.panel_central, width=ANCHO_PANEL_LATERAL)
|
||||
self.panel_lateral.grid(row=0, column=0, sticky="nswe", padx=(10, 5), pady=10)
|
||||
|
||||
self.panel_lateral.grid_propagate(False)
|
||||
|
||||
# --- LÓGICA DE ACTUALIZACIÓN DE RELOJ ---
|
||||
def actualizar_reloj(self):
|
||||
"""Obtiene la hora local y actualiza la etiqueta del reloj."""
|
||||
"""Obtiene la hora local y actualiza la etiqueta del reloj. CORREGIDO: No se detiene en caso de error."""
|
||||
try:
|
||||
hora_actual = obtener_hora_local()
|
||||
if self.label_reloj:
|
||||
|
|
@ -99,10 +103,10 @@ class VentanaPrincipal(tk.Tk):
|
|||
if self.label_reloj:
|
||||
self.label_reloj.config(text="Error al obtener la hora")
|
||||
print(f"Error en el reloj: {e}")
|
||||
self.detener_actualizacion_reloj()
|
||||
return
|
||||
# CORRECCIÓN: Eliminamos la detención para que el after continúe intentando
|
||||
# self.detener_actualizacion_reloj()
|
||||
# return
|
||||
|
||||
# Usando la constante importada
|
||||
self.reloj_after_id = self.after(INTERVALO_RELOJ_MS, self.actualizar_reloj)
|
||||
|
||||
def iniciar_actualizacion_reloj(self):
|
||||
|
|
@ -119,7 +123,7 @@ class VentanaPrincipal(tk.Tk):
|
|||
|
||||
# --- LÓGICA DE ACTUALIZACIÓN DE CLIMA ---
|
||||
def actualizar_clima(self):
|
||||
"""Obtiene la temperatura local y actualiza la etiqueta en la barra inferior."""
|
||||
"""Obtiene la temperatura local y actualiza la etiqueta en la barra inferior. CORREGIDO: No se detiene en caso de error."""
|
||||
try:
|
||||
datos_clima = obtener_datos_clima()
|
||||
if self.label_clima:
|
||||
|
|
@ -128,10 +132,10 @@ class VentanaPrincipal(tk.Tk):
|
|||
if self.label_clima:
|
||||
self.label_clima.config(text="Error al obtener el clima")
|
||||
print(f"Error en la actualización del clima: {e}")
|
||||
self.detener_actualizacion_clima()
|
||||
return
|
||||
# CORRECCIÓN: Eliminamos la detención para que el after continúe intentando
|
||||
# self.detener_actualizacion_clima()
|
||||
# return
|
||||
|
||||
# Usando la constante importada
|
||||
self.clima_after_id = self.after(INTERVALO_CLIMA_MS, self.actualizar_clima)
|
||||
|
||||
def iniciar_actualizacion_clima(self):
|
||||
|
|
@ -152,6 +156,7 @@ class VentanaPrincipal(tk.Tk):
|
|||
self.detener_actualizacion_reloj()
|
||||
self.detener_actualizacion_clima()
|
||||
|
||||
# Solo intenta detener el panel central si fue inicializado
|
||||
if self.panel_central:
|
||||
self.panel_central.detener_actualizacion_automatica()
|
||||
|
||||
|
|
@ -183,4 +188,135 @@ class VentanaPrincipal(tk.Tk):
|
|||
frame_fecha.grid(row=0, column=2, sticky="e")
|
||||
|
||||
self.label_reloj = ttk.Label(frame_fecha, text="Día y Hora: --/--/--", style='TLabel')
|
||||
self.label_reloj.pack(side="left")
|
||||
self.label_reloj.pack(side="left")# Módulo: vista/central_panel/view_radio.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox
|
||||
|
||||
# 🎯 Asumo que MusicReproductor está importado aquí.
|
||||
from logica.T2.musicReproductor import MusicReproductor
|
||||
from vista.config import *
|
||||
|
||||
|
||||
class RadioPanel(ttk.Frame):
|
||||
"""
|
||||
Panel de controles de Radio/Música.
|
||||
Gestiona la interfaz de reproducción y volumen.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_frame_musica, root, *args, **kwargs):
|
||||
super().__init__(parent_frame_musica, *args, **kwargs)
|
||||
self.root = root
|
||||
|
||||
# 🎯 Instanciar la lógica del reproductor al inicializar la vista
|
||||
self.reproductor = MusicReproductor()
|
||||
|
||||
# Variables de control de UI
|
||||
self.volumen_var = tk.DoubleVar(value=self.reproductor.get_volumen()) # Inicializa al volumen actual (por defecto 50)
|
||||
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.crear_interfaz_radio(self)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 🖼️ INTERFAZ DE USUARIO
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def crear_interfaz_radio(self, parent_frame):
|
||||
"""Crea los controles de reproducción y el slider de volumen."""
|
||||
|
||||
main_frame = ttk.Frame(parent_frame, padding=5, style='TFrame')
|
||||
main_frame.grid(row=0, column=0, sticky="nsew")
|
||||
main_frame.grid_columnconfigure(0, weight=1) # Columna de botones
|
||||
main_frame.grid_columnconfigure(1, weight=1) # Columna de botones
|
||||
main_frame.grid_columnconfigure(2, weight=1) # Columna de botones
|
||||
main_frame.grid_columnconfigure(3, weight=1) # Columna de volumen
|
||||
|
||||
# --- Título ---
|
||||
ttk.Label(main_frame, text="Controles de Música", font=FUENTE_NEGOCIOS).grid(
|
||||
row=0, column=0, columnspan=4, pady=(0, 10), sticky="w")
|
||||
|
||||
|
||||
# --- Botones de Control (Fila 1) ---
|
||||
|
||||
# Botón Play/Pause
|
||||
self.boton_play_pause = ttk.Button(main_frame, text="▶️", style='Action.TButton', command=self.manejar_play_pause)
|
||||
self.boton_play_pause.grid(row=1, column=1, sticky="ew", padx=5)
|
||||
|
||||
# Botón Stop
|
||||
ttk.Button(main_frame, text="⏹️", style='Action.TButton', command=self.manejar_stop).grid(
|
||||
row=1, column=2, sticky="ew", padx=5)
|
||||
|
||||
# Botón de Prueba de Carga (Para probar la reproducción de una URL)
|
||||
ttk.Button(main_frame, text="📡 Cargar Stream", command=self.cargar_stream_prueba).grid(
|
||||
row=1, column=0, sticky="ew", padx=5)
|
||||
|
||||
# --- Slider de Volumen (Fila 2) ---
|
||||
ttk.Label(main_frame, text="Volumen:", font=FUENTE_NORMAL).grid(
|
||||
row=2, column=0, columnspan=4, pady=(10, 0), sticky="w")
|
||||
|
||||
self.slider_volumen = ttk.Scale(
|
||||
main_frame,
|
||||
from_=0,
|
||||
to=100,
|
||||
orient="horizontal",
|
||||
variable=self.volumen_var,
|
||||
command=self.manejar_ajuste_volumen
|
||||
)
|
||||
self.slider_volumen.grid(row=3, column=0, columnspan=4, sticky="ew", pady=(5, 0))
|
||||
|
||||
# Etiqueta de la estación actual (para estado)
|
||||
self.label_estado = ttk.Label(main_frame, text="Estado: Detenido", anchor="center", font=('Arial', 9))
|
||||
self.label_estado.grid(row=4, column=0, columnspan=4, pady=(5, 0), sticky="ew")
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# ⏯️ MANEJO DE LA LÓGICA
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def manejar_play_pause(self):
|
||||
"""Alterna entre reproducir y pausar."""
|
||||
if self.reproductor.esta_reproduciendo():
|
||||
self.reproductor.pausar()
|
||||
self.boton_play_pause.config(text="▶️")
|
||||
self.label_estado.config(text="Estado: Pausado")
|
||||
else:
|
||||
# Si está pausado o detenido, intenta reproducir el último stream cargado.
|
||||
if self.reproductor.continuar():
|
||||
self.boton_play_pause.config(text="⏸️")
|
||||
self.label_estado.config(text="Estado: Reproduciendo")
|
||||
else:
|
||||
# Si no hay stream cargado, se mantiene detenido o se puede mostrar un error.
|
||||
messagebox.showwarning("Advertencia", "No hay stream cargado para reproducir.")
|
||||
|
||||
|
||||
def manejar_stop(self):
|
||||
"""Detiene completamente la reproducción."""
|
||||
self.reproductor.detener()
|
||||
self.boton_play_pause.config(text="▶️")
|
||||
self.label_estado.config(text="Estado: Detenido")
|
||||
|
||||
def manejar_ajuste_volumen(self, valor):
|
||||
"""Ajusta el volumen del reproductor basado en el slider."""
|
||||
volumen = int(float(valor))
|
||||
self.reproductor.set_volumen(volumen)
|
||||
# Puedes añadir una pequeña etiqueta para ver el volumen si es necesario.
|
||||
print(f"Volumen ajustado a: {volumen}%")
|
||||
|
||||
def cargar_stream_prueba(self):
|
||||
"""
|
||||
Carga y reproduce una URL de prueba o la última guardada.
|
||||
Aquí usamos una URL de ejemplo que puede ser más estable.
|
||||
"""
|
||||
# Nota: La URL de tu log falló. Usamos una de prueba conocida (Radio Paradise)
|
||||
URL_STREAM_PRUEBA = "http://stream.radioparadise.com/flac"
|
||||
|
||||
success, mensaje = self.reproductor.cargar_y_reproducir(URL_STREAM_PRUEBA)
|
||||
|
||||
if success:
|
||||
self.boton_play_pause.config(text="⏸️")
|
||||
self.label_estado.config(text=f"Estado: Reproduciendo {mensaje}")
|
||||
else:
|
||||
self.label_estado.config(text=f"Error: {mensaje}")
|
||||
messagebox.showerror("Error de Stream", f"No se pudo cargar el stream. Revisa la URL y la conexión.\nDetalle: {mensaje}")
|
||||
Loading…
Reference in New Issue