feat(vista): Integra radios y refactoriza (main)

Integra T3: Reproductor de radios con control de volumen.
* Añade pestaña "Radios" con lista de emisoras.
* Implementa selección y reproducción de streams.
* Permite ajustar el volumen.
* Refactoriza panel central para integrar la app.
* Agrega controles de reproductor al panel lateral.
* Corrige rutas de ficheros y nombres de variables.
* Actualiza readme con dependencias.
```
This commit is contained in:
BYolivia 2025-12-04 16:14:48 +01:00
parent 813b2b7d65
commit 983836d94c
7 changed files with 410 additions and 52 deletions

View File

@ -2,7 +2,11 @@
tkinter => hay que instalar python3-tk tkinter => hay que instalar python3-tk
matplotlib => pip install matplotlib matplotlib => pip install matplotlib
psutil => pip install psutil psutil => pip install psutil
python-vlc => pip install python-vlc
## Como Ejecutar ## ## Como Ejecutar ##
> [!NOTE] > [!NOTE]
> Desde la carpeta anterior > Desde la carpeta anterior
@ -28,12 +32,14 @@ python -m ProyectoGlobal
### T2. Multihilos ### ### T2. Multihilos ###
1. Hora del sistema / Fecha del sistema 1. ~~Hora del sistema / Fecha del sistema~~
2. Programar Alarma (aviso visual y sonoro al pasar X minutos) 2. ~~Temperatura local~~
3. Scraping 3. Programar Alarma (aviso visual y sonoro al pasar X minutos)
4. Juego de los camellos / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos) 4. Scraping
5. Música de fondo (reproducción de mp3 o midi) 5. Juego de los camellos / autos de choque / etc. (aplicar resolución de sincronización para evitar problemas de interbloqueos)
6. Música de fondo (reproducción de mp3 o midi)

View File

@ -0,0 +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:
"""
Gestiona la reproducción de streams de radio usando la librería python-vlc.
"""
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()
self.player = self.instance.media_player_new()
self.current_media = None
self.is_playing = False
# Configurar volumen inicial
self.ajustar_volumen(initial_volume)
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.player.audio_get_volume()}")
def ajustar_volumen(self, valor_porcentual):
"""
Ajusta el volumen del reproductor (0 a 100).
"""
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
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
print(f"🔄 [VLC] Intentando cargar y reproducir: {url_stream}")
self.player.stop()
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] 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 y libera los recursos. Crucial al cerrar la aplicación.
"""
if self.player:
self.player.stop()
# Limpiar referencias para asegurar que VLC se libere correctamente
del self.player
del self.instance
print("⏹️ [VLC] Reproductor detenido y recursos liberados.")

106
logica/auxiliar/__main__.py Normal file
View File

@ -0,0 +1,106 @@
import json
import os
# --- NOMBRE DEL ARCHIVO ---
NOMBRE_FICHERO = "radios.json"
def cargar_emisoras():
"""Carga las emisoras existentes desde el archivo, o retorna una lista vacía si no existe."""
if os.path.exists(NOMBRE_FICHERO):
try:
with open(NOMBRE_FICHERO, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError:
print(f"⚠️ Error al leer el archivo {NOMBRE_FICHERO}. Creando una lista nueva.")
return []
return []
def guardar_emisoras(emisoras):
"""Guarda la lista de emisoras en formato JSON en el archivo."""
try:
with open(NOMBRE_FICHERO, 'w', encoding='utf-8') as f:
# Usamos indent=4 para que el JSON sea legible
json.dump(emisoras, f, indent=4, ensure_ascii=False)
print(f"\n✅ ¡Éxito! El archivo '{NOMBRE_FICHERO}' ha sido guardado.")
except Exception as e:
print(f"\n❌ Error al intentar guardar el archivo: {e}")
def crear_nueva_emisora(emisoras):
"""Pide al usuario los datos de una nueva emisora y la añade a la lista."""
print("\n--- Crear Nueva Emisora ---")
# --- CAMPOS OBLIGATORIOS ---
nombre = input("▶️ Nombre de la radio (ej: Jazz Clásico): ").strip()
url = input("▶️ URL del Stream (ej: http://stream.com/live.mp3): ").strip()
if not nombre or not url:
print("❌ El nombre y la URL son obligatorios. Inténtalo de nuevo.")
return
# --- CAMPOS OPCIONALES ---
pais = input("▶️ País (ej: ES, DE) [Opcional]: ").strip()
genero = input("▶️ Género (ej: Pop, Noticias) [Opcional]: ").strip()
nueva_emisora = {
"nombre": nombre,
"url_stream": url,
"pais": pais if pais else None,
"genero": genero if genero else None
}
emisoras.append(nueva_emisora)
print(f"\n✅ Emisora '{nombre}' añadida a la lista en memoria.")
def mostrar_emisoras(emisoras):
"""Muestra la lista de emisoras actuales en memoria."""
if not emisoras:
print("\nLista de emisoras vacía. Utiliza la opción 1 para añadir una.")
return
print(f"\n--- Emisoras Actuales en Memoria ({len(emisoras)} en total) ---")
for i, e in enumerate(emisoras):
print(f"\nID: {i + 1}")
print(f" Nombre: {e.get('nombre', 'N/D')}")
print(f" URL: {e.get('url_stream', 'N/D')}")
print(f" País: {e.get('pais', 'N/D')}")
print(f" Género: {e.get('genero', 'N/D')}")
print("--------------------------------------------------")
def menu_principal():
"""Función principal que ejecuta el menú interactivo."""
# Cargar las emisoras al iniciar (si el archivo existe)
emisoras_en_memoria = cargar_emisoras()
while True:
print("\n" + "=" * 30)
print("📻 EDITOR DE CONFIGURACIÓN DE RADIOS 📻")
print("=" * 30)
print("1. Crear y Añadir Nueva Emisora")
print("2. Mostrar Emisoras en Memoria")
print(f"3. Guardar Lista en '{NOMBRE_FICHERO}'")
print("4. Salir (Sin guardar los cambios en memoria)")
print("-" * 30)
opcion = input("Elige una opción (1-4): ").strip()
if opcion == '1':
crear_nueva_emisora(emisoras_en_memoria)
elif opcion == '2':
mostrar_emisoras(emisoras_en_memoria)
elif opcion == '3':
guardar_emisoras(emisoras_en_memoria)
elif opcion == '4':
print("\nSaliendo del editor. ¡Hasta pronto!")
break
else:
print("Opción no válida. Por favor, selecciona un número del 1 al 4.")
if __name__ == "__main__":
menu_principal()

View File

@ -0,0 +1,3 @@
Aqui se alojan pequeños programas que no tienen que ver con el
programa pero pueden simplificar procesos como
es crear un fichero con datos que tiene que leer el programa

20
res/radios.json Normal file
View File

@ -0,0 +1,20 @@
[
{
"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
}
]

View File

@ -3,32 +3,63 @@
import random import random
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import json
import os
import sys # Necesario para el bloque opcional de VLC
# --- LÓGICA DE T1 (MONITOR DE RECURSOS) ---
from logica.T1.trafficMeter import iniciar_monitor_red from logica.T1.trafficMeter import iniciar_monitor_red
from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos
from matplotlib.figure import Figure from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# --- IMPORTACIÓN DE LA LÓGICA DE T2 (CARRERA DE CAMELLOS) --- # --- LÓGICA DE T2 (CARRERA DE CAMELLOS) ---
from logica.T2.carreraCamellos import ( from logica.T2.carreraCamellos import (
iniciar_carrera, iniciar_carrera,
obtener_estado_carrera, obtener_estado_carrera,
detener_carrera, RESULTADO_ULTIMO
RESULTADO_ULTIMO # Variable de estado persistente
) )
# --- LÓGICA DE T3 (REPRODUCTOR DE MÚSICA) ---
# Se necesita el bloque opcional aquí, ya que el import 'vlc' está en este módulo
try:
from logica.T2.musicReproductor import MusicReproductor
except ImportError:
# Bloque OPCIONAL si falla el import de MusicReproductor por problemas de VLC en el entorno
print("⚠️ Error al importar MusicReproductor. Asegúrate de tener 'python-vlc' instalado y VLC en tu sistema.")
class MusicReproductor:
def __init__(self, *args, **kwargs): pass
def ajustar_volumen(self, valor): print(f"Volumen (Simulado): {valor}")
def cargar_y_reproducir(self, url): print(f"Reproduciendo (Simulado): {url}")
def reproducir(self): print("Reproducir (Simulado)")
def pausar(self): print("Pausar (Simulado)")
def detener(self): print("Detener (Simulado)")
# --- IMPORTACIÓN UNIVERSAL DE CONSTANTES --- # --- IMPORTACIÓN UNIVERSAL DE CONSTANTES ---
# Asume que todas las constantes de estilo y colores necesarias están definidas en vista.config
from vista.config import * from vista.config import *
class PanelCentral(ttk.Frame): class PanelCentral(ttk.Frame):
"""Contiene el Notebook (subpestañas de T1, T2 en Resultados), el panel de Notas y el panel de Chat.""" """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 y T3."""
INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS
INTERVALO_CARRERA_MS = 200 INTERVALO_CARRERA_MS = 200
def __init__(self, parent, *args, **kwargs): # ✅ CORRECCIÓN DE RUTA
NOMBRE_FICHERO_RADIOS = "res/radios.json"
def __init__(self, parent, root, *args, **kwargs):
super().__init__(parent, *args, **kwargs) super().__init__(parent, *args, **kwargs)
self.root = root
self.after_id = None self.after_id = None
self.after_carrera_id = None self.after_carrera_id = None
self.camellos = [] self.camellos = []
@ -43,7 +74,13 @@ class PanelCentral(ttk.Frame):
self.figure = Figure(figsize=(5, 4), dpi=100) self.figure = Figure(figsize=(5, 4), dpi=100)
self.canvas = None self.canvas = None
# 2. CONFIGURACIÓN DEL LAYOUT # 2. INICIALIZACIÓN DE VARIABLES Y LÓGICA DE RADIO (T3)
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())
# 3. CONFIGURACIÓN DEL LAYOUT
self.grid_columnconfigure(0, weight=3) self.grid_columnconfigure(0, weight=3)
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1)
@ -51,9 +88,99 @@ class PanelCentral(ttk.Frame):
self.crear_area_principal_y_notas() self.crear_area_principal_y_notas()
self.crear_panel_chat_y_alumnos() self.crear_panel_chat_y_alumnos()
# 4. INICIO DE CICLOS DE ACTUALIZACIÓN
self.iniciar_actualizacion_automatica() self.iniciar_actualizacion_automatica()
self.iniciar_actualizacion_carrera() self.iniciar_actualizacion_carrera()
# -------------------------------------------------------------
# 📻 LÓGICA Y VISTA DE T3 (REPRODUCTOR DE RADIOS)
# -------------------------------------------------------------
def cargar_emisoras(self):
"""Carga la lista de emisoras desde el archivo radios.json."""
try:
# ✅ La ruta ahora apunta correctamente al subdirectorio 'res'
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")
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']
# 1. Actualizar la interfaz
self.radio_seleccionada.set(emisora['nombre'])
self.url_seleccionada_label.config(text=url)
# 2. Llamar a la lógica del reproductor
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):
"""
Llama al método de control de volumen del reproductor,
asegurando que el valor sea un entero para evitar saltos del Scale.
"""
# ✅ CORRECCIÓN DE LA BARRA DE VOLUMEN
# 1. Convertir el valor de punto flotante a entero
valor_entero = int(float(valor))
# 2. Actualizar la variable de control con el valor entero.
self.volumen_var.set(valor_entero)
# 3. Llamar a la lógica del reproductor.
self.reproductor.ajustar_volumen(valor_entero)
# ------------------------------------------------------------- # -------------------------------------------------------------
# 📦 ESTRUCTURA PRINCIPAL DEL PANEL # 📦 ESTRUCTURA PRINCIPAL DEL PANEL
# ------------------------------------------------------------- # -------------------------------------------------------------
@ -77,11 +204,11 @@ class PanelCentral(ttk.Frame):
expand=True, fill="both") expand=True, fill="both")
def crear_notebook_pestañas(self, parent_frame): def crear_notebook_pestañas(self, parent_frame):
"""Crea las pestañas internas para las tareas (T1, Carrera en Resultados).""" """Crea las pestañas internas para las tareas (T1, Carrera, Radios)."""
sub_notebook = ttk.Notebook(parent_frame) sub_notebook = ttk.Notebook(parent_frame)
sub_notebook.grid(row=0, column=0, sticky="nsew") sub_notebook.grid(row=0, column=0, sticky="nsew")
sub_tabs = ["Recursos", "Resultados", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"] sub_tabs = ["Recursos", "Resultados", "Radios", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"]
self.tabs = {} self.tabs = {}
for i, sub_tab_text in enumerate(sub_tabs): for i, sub_tab_text in enumerate(sub_tabs):
@ -90,7 +217,6 @@ class PanelCentral(ttk.Frame):
self.tabs[sub_tab_text] = frame self.tabs[sub_tab_text] = frame
if sub_tab_text == "Recursos": if sub_tab_text == "Recursos":
# Lógica de T1 - Gráfico de recursos
self.grafico_frame = ttk.Frame(frame, style='TFrame') self.grafico_frame = ttk.Frame(frame, style='TFrame')
self.grafico_frame.pack(expand=True, fill="both", padx=10, pady=10) self.grafico_frame.pack(expand=True, fill="both", padx=10, pady=10)
@ -98,18 +224,18 @@ class PanelCentral(ttk.Frame):
self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(expand=True, fill="both") self.canvas_widget.pack(expand=True, fill="both")
# --- INTEGRACIÓN DE LA CARRERA EN LA PESTAÑA "Resultados" ---
elif sub_tab_text == "Resultados": elif sub_tab_text == "Resultados":
self.crear_interfaz_carrera(frame) self.crear_interfaz_carrera(frame)
elif sub_tab_text == "Radios":
self.crear_interfaz_radios(frame)
# ------------------------------------------------------------- # -------------------------------------------------------------
# 🐪 LÓGICA DE T2 (CARRERA DE CAMELLOS) # 🐪 LÓGICA DE T2 (CARRERA DE CAMELLOS)
# ------------------------------------------------------------- # -------------------------------------------------------------
def crear_interfaz_carrera(self, parent_frame): def crear_interfaz_carrera(self, parent_frame):
"""Crea los controles y la visualización de la Carrera de Camellos.""" """Crea los controles y la visualización de la Carrera de Camellos."""
# 1. Título y Controles (Solo título, el botón de inicio va en el lateral)
frame_controles = ttk.Frame(parent_frame, style='TFrame', padding=10) frame_controles = ttk.Frame(parent_frame, style='TFrame', padding=10)
frame_controles.pack(fill="x") frame_controles.pack(fill="x")
self.frame_carrera_controles = frame_controles self.frame_carrera_controles = frame_controles
@ -120,7 +246,6 @@ class PanelCentral(ttk.Frame):
self.carrera_estado_label = ttk.Label(frame_controles, text="Estado.", style='TLabel', font=FUENTE_NEGOCIOS) 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.carrera_estado_label.pack(side="right", padx=10)
# 2. Marco de visualización de progreso
self.frame_progreso = ttk.Frame(parent_frame, style='TFrame', padding=10) self.frame_progreso = ttk.Frame(parent_frame, style='TFrame', padding=10)
self.frame_progreso.pack(fill="both", expand=True) self.frame_progreso.pack(fill="both", expand=True)
@ -136,18 +261,15 @@ class PanelCentral(ttk.Frame):
print("Iniciando Carrera de Camellos con número variable de participantes...") print("Iniciando Carrera de Camellos con número variable de participantes...")
# Genera un número aleatorio de camellos entre 10 y 20
num_camellos = random.randint(10, 20) num_camellos = random.randint(10, 20)
nombres = [f"Camello {i + 1}" for i in range(num_camellos)] nombres = [f"Camello {i + 1}" for i in range(num_camellos)]
# Limpiar la visualización anterior
for widget in self.frame_progreso.winfo_children(): for widget in self.frame_progreso.winfo_children():
widget.destroy() widget.destroy()
self.progreso_labels = {} self.progreso_labels = {}
self.camellos = iniciar_carrera(nombres) self.camellos = iniciar_carrera(nombres)
# --- MENSAJE SIMPLIFICADO ---
self.carrera_estado_label.config(text=f"¡Carrera en marcha! 💨 ({num_camellos} participantes)") self.carrera_estado_label.config(text=f"¡Carrera en marcha! 💨 ({num_camellos} participantes)")
self.crear_visualizacion_carrera(nombres) self.crear_visualizacion_carrera(nombres)
@ -199,7 +321,6 @@ class PanelCentral(ttk.Frame):
if etiqueta_posicion and estado['posicion']: if etiqueta_posicion and estado['posicion']:
etiqueta_posicion.config(text=f"POS: {estado['posicion']} 🏆", foreground=COLOR_ACCION) etiqueta_posicion.config(text=f"POS: {estado['posicion']} 🏆", foreground=COLOR_ACCION)
# --- MENSAJE SIMPLIFICADO ---
self.carrera_estado_label.config(text="Carrera en curso...") self.carrera_estado_label.config(text="Carrera en curso...")
self.carrera_info_label.config(text="") self.carrera_info_label.config(text="")
@ -226,7 +347,6 @@ class PanelCentral(ttk.Frame):
etiqueta_posicion.config(text=f"POS: {estado['posicion']} {'🏆' if estado['posicion'] == 1 else ''}", etiqueta_posicion.config(text=f"POS: {estado['posicion']} {'🏆' if estado['posicion'] == 1 else ''}",
foreground=COLOR_ACCION) foreground=COLOR_ACCION)
# --- MENSAJE SIMPLIFICADO ---
self.carrera_estado_label.config(text="✅ Carrera Terminada.") self.carrera_estado_label.config(text="✅ Carrera Terminada.")
self.carrera_info_label.config(text=f"¡El ganador es: {resultado_final['ganador']}!", self.carrera_info_label.config(text=f"¡El ganador es: {resultado_final['ganador']}!",
font=FUENTE_TITULO, foreground=COLOR_EXITO) font=FUENTE_TITULO, foreground=COLOR_EXITO)
@ -253,7 +373,6 @@ class PanelCentral(ttk.Frame):
if RESULTADO_ULTIMO and not RESULTADO_ULTIMO.get('activa', True) and self.frame_progreso: if RESULTADO_ULTIMO and not RESULTADO_ULTIMO.get('activa', True) and self.frame_progreso:
self.mostrar_resultado_final(RESULTADO_ULTIMO) self.mostrar_resultado_final(RESULTADO_ULTIMO)
else: else:
# --- MENSAJE SIMPLIFICADO AL CARGAR ---
self.carrera_estado_label.config(text="Carrera lista para empezar.") self.carrera_estado_label.config(text="Carrera lista para empezar.")
self.after_carrera_id = self.after(0, self.actualizar_carrera) self.after_carrera_id = self.after(0, self.actualizar_carrera)
@ -295,7 +414,7 @@ class PanelCentral(ttk.Frame):
self.after_id = self.after(0, self.actualizar_recursos) self.after_id = self.after(0, self.actualizar_recursos)
def detener_actualizacion_automatica(self): def detener_actualizacion_automatica(self):
"""Detiene el ciclo de actualización periódica y el hilo de red (T1) y la carrera (T2).""" """Detiene el ciclo de actualización periódica y el hilo de red (T1) y T3."""
if self.after_id: if self.after_id:
self.after_cancel(self.after_id) self.after_cancel(self.after_id)
self.after_id = None self.after_id = None
@ -308,31 +427,31 @@ class PanelCentral(ttk.Frame):
self.detener_actualizacion_carrera() self.detener_actualizacion_carrera()
# Detener el reproductor al cerrar la aplicación
if self.reproductor:
self.reproductor.detener()
# ------------------------------------------------------------- # -------------------------------------------------------------
# 💬 PANEL CHAT, ALUMNOS Y APPS (PANEL LATERAL) # 💬 PANEL CHAT, ALUMNOS Y APPS (PANEL LATERAL)
# ------------------------------------------------------------- # -------------------------------------------------------------
def crear_panel_chat_y_alumnos(self, ): def crear_panel_chat_y_alumnos(self, ):
"""Crea el panel de chat y lista de Alumnos (columna derecha).""" """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 = ttk.Frame(self, style='TFrame', padding="10")
panel_chat.grid(row=0, column=1, sticky="nsew") panel_chat.grid(row=0, column=1, sticky="nsew")
# Configuración de expansión (La fila 5, que contiene los alumnos, es la que se expande)
panel_chat.grid_rowconfigure(5, weight=1) panel_chat.grid_rowconfigure(5, weight=1)
panel_chat.grid_columnconfigure(0, weight=1) panel_chat.grid_columnconfigure(0, weight=1)
# --- FILA 0: Título --- # --- FILA 0: Título ---
ttk.Label(panel_chat, text="Chat", foreground=COLOR_ACCION, font=FUENTE_TITULO, style='TLabel').grid(row=0, ttk.Label(panel_chat, text="Chat", foreground=COLOR_ACCION, font=FUENTE_TITULO, style='TLabel').grid(row=0,
column=0, column=0,
pady=( pady=(0,
0, 10),
10),
sticky="w") sticky="w")
# --- FILAS 1-3: Chat Input --- # --- FILAS 1-3: Chat Input ---
ttk.Label(panel_chat, text="Mensaje", style='TLabel').grid(row=1, column=0, sticky="w") ttk.Label(panel_chat, text="Mensaje", style='TLabel').grid(row=1, column=0, sticky="w")
# Usando COLOR_BLANCO para el fondo del Text
chat_text = tk.Text(panel_chat, height=6, width=30, bg=COLOR_BLANCO, relief="solid", borderwidth=1, chat_text = tk.Text(panel_chat, height=6, width=30, bg=COLOR_BLANCO, relief="solid", borderwidth=1,
font=('Arial', 10)) font=('Arial', 10))
chat_text.grid(row=2, column=0, sticky="ew", pady=(0, 5)) chat_text.grid(row=2, column=0, sticky="ew", pady=(0, 5))
@ -352,26 +471,33 @@ class PanelCentral(ttk.Frame):
ttk.Button(frame_alumno, text="", width=3, style='Action.TButton').grid(row=0, column=1, rowspan=2, padx=5, ttk.Button(frame_alumno, text="", width=3, style='Action.TButton').grid(row=0, column=1, rowspan=2, padx=5,
sticky="ne") sticky="ne")
# --- FILA 8: Música --- # --- FILA 8: Reproductor Música ---
musica_frame = ttk.LabelFrame(panel_chat, text="Reproductor Música", padding=10, style='TFrame') 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)) musica_frame.grid(row=8, column=0, sticky="ew", pady=(15, 0))
ttk.Label(musica_frame, text="[ Botones de Play/Stop y Control de Volumen ]", anchor="center",
style='TLabel').pack(fill="x", padx=5, pady=5)
# --- FILA 9: Botones de Aplicación (Minijuegos) --- # 8a: Emisora Actual
frame_apps = ttk.LabelFrame(panel_chat, text="Minijuegos / Apps", padding=10, style='TFrame') ttk.Label(musica_frame, text="Actual: ", style='TLabel').grid(row=0, column=0, sticky="w")
frame_apps.grid(row=9, column=0, sticky="ew", pady=(15, 0)) ttk.Label(musica_frame, textvariable=self.radio_seleccionada, font=('Arial', 10, 'bold'), style='TLabel').grid(
row=0, column=1, columnspan=2, sticky="w")
# Botón 1: Placeholder # 8b: Controles de Reproducción (Play/Pause)
ttk.Button(frame_apps, text="App1 (T3-Red)", width=15, style='Action.TButton').pack(side="left", padx=5) frame_controles_musica = ttk.Frame(musica_frame, style='TFrame')
frame_controles_musica.grid(row=1, column=0, columnspan=3, pady=(5, 5))
# Botón 2: Inicia la Carrera de Camellos (T2 Sincronización) ttk.Button(frame_controles_musica, text="▶️ Iniciar", command=lambda: self.controlar_reproduccion('play'),
ttk.Button(frame_apps, style='Action.TButton', width=8).pack(side="left", padx=5)
text="App2 (T2-Carrera 🏁)", ttk.Button(frame_controles_musica, text="⏸️ Pausar", command=lambda: self.controlar_reproduccion('pause'),
command=self.manejar_inicio_carrera, style='Action.TButton', width=8).pack(side="left", padx=5)
width=15,
style='Action.TButton').pack(side="left", padx=5)
# Asegurar que las filas de abajo no se expandan # 8c: Control de Volumen (Scale/Deslizable)
panel_chat.grid_rowconfigure(8, weight=0) ttk.Label(musica_frame, text="Volumen:", style='TLabel').grid(row=2, column=0, sticky="w")
panel_chat.grid_rowconfigure(9, weight=0)
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)
panel_chat.grid_rowconfigure(8, weight=0)

View File

@ -16,7 +16,7 @@ class VentanaPrincipal(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("Proyecto Integrado - PSP (Estilo Moderno Nativo)") self.title("Proyecto Integrado - PSP")
self.geometry("1200x800") self.geometry("1200x800")
self.label_reloj = None self.label_reloj = None
@ -79,7 +79,7 @@ class VentanaPrincipal(tk.Tk):
def crear_paneles_principales(self): 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 en la rejilla, asegurando el ancho del lateral."""
self.panel_central = PanelCentral(self) self.panel_central = PanelCentral(self, self)
self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10) self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10)
# Usando la constante importada # Usando la constante importada