From 983836d94ca14a9e59dddabee1bd1620c7094b76 Mon Sep 17 00:00:00 2001 From: BYolivia Date: Thu, 4 Dec 2025 16:14:48 +0100 Subject: [PATCH] ``` feat(vista): Integra radios y refactoriza (main) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. ``` --- Readme.md | 16 ++- logica/T2/musicReproductor.py | 97 +++++++++++++++ logica/auxiliar/__main__.py | 106 +++++++++++++++++ logica/auxiliar/readme.md | 3 + res/radios.json | 20 ++++ vista/panel_central.py | 216 +++++++++++++++++++++++++++------- vista/ventana_principal.py | 4 +- 7 files changed, 410 insertions(+), 52 deletions(-) create mode 100644 logica/auxiliar/__main__.py create mode 100644 logica/auxiliar/readme.md create mode 100644 res/radios.json diff --git a/Readme.md b/Readme.md index 770f074..effb622 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,11 @@ tkinter => hay que instalar python3-tk matplotlib => pip install matplotlib + psutil => pip install psutil + +python-vlc => pip install python-vlc + ## Como Ejecutar ## > [!NOTE] > Desde la carpeta anterior @@ -28,12 +32,14 @@ python -m ProyectoGlobal ### 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) \ No newline at end of file +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) \ No newline at end of file diff --git a/logica/T2/musicReproductor.py b/logica/T2/musicReproductor.py index e69de29..c319e0c 100644 --- a/logica/T2/musicReproductor.py +++ b/logica/T2/musicReproductor.py @@ -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.") \ No newline at end of file diff --git a/logica/auxiliar/__main__.py b/logica/auxiliar/__main__.py new file mode 100644 index 0000000..9dad83f --- /dev/null +++ b/logica/auxiliar/__main__.py @@ -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() \ No newline at end of file diff --git a/logica/auxiliar/readme.md b/logica/auxiliar/readme.md new file mode 100644 index 0000000..4aa38e6 --- /dev/null +++ b/logica/auxiliar/readme.md @@ -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 \ No newline at end of file diff --git a/res/radios.json b/res/radios.json new file mode 100644 index 0000000..2ada0f6 --- /dev/null +++ b/res/radios.json @@ -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 + } +] \ No newline at end of file diff --git a/vista/panel_central.py b/vista/panel_central.py index 395b549..a7895f2 100644 --- a/vista/panel_central.py +++ b/vista/panel_central.py @@ -3,32 +3,63 @@ import random import tkinter as tk 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.graficos import crear_grafico_recursos, actualizar_historial_datos from matplotlib.figure import Figure 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 ( iniciar_carrera, obtener_estado_carrera, - detener_carrera, - RESULTADO_ULTIMO # Variable de estado persistente + RESULTADO_ULTIMO ) +# --- 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 --- -# Asume que todas las constantes de estilo y colores necesarias están definidas en vista.config from vista.config import * 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_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) + self.root = root + self.after_id = None self.after_carrera_id = None self.camellos = [] @@ -43,7 +74,13 @@ class PanelCentral(ttk.Frame): self.figure = Figure(figsize=(5, 4), dpi=100) 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(1, weight=1) self.grid_rowconfigure(0, weight=1) @@ -51,9 +88,99 @@ class PanelCentral(ttk.Frame): self.crear_area_principal_y_notas() self.crear_panel_chat_y_alumnos() + # 4. INICIO DE CICLOS DE ACTUALIZACIÓN self.iniciar_actualizacion_automatica() 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('<>', 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 # ------------------------------------------------------------- @@ -77,11 +204,11 @@ class PanelCentral(ttk.Frame): expand=True, fill="both") 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.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 = {} for i, sub_tab_text in enumerate(sub_tabs): @@ -90,7 +217,6 @@ class PanelCentral(ttk.Frame): self.tabs[sub_tab_text] = frame if sub_tab_text == "Recursos": - # Lógica de T1 - Gráfico de recursos self.grafico_frame = ttk.Frame(frame, style='TFrame') 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.pack(expand=True, fill="both") - # --- INTEGRACIÓN DE LA CARRERA EN LA PESTAÑA "Resultados" --- elif sub_tab_text == "Resultados": self.crear_interfaz_carrera(frame) + elif sub_tab_text == "Radios": + self.crear_interfaz_radios(frame) + # ------------------------------------------------------------- # 🐪 LÓGICA DE T2 (CARRERA DE CAMELLOS) # ------------------------------------------------------------- def crear_interfaz_carrera(self, parent_frame): """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.pack(fill="x") 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.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.pack(fill="both", expand=True) @@ -136,18 +261,15 @@ class PanelCentral(ttk.Frame): 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) nombres = [f"Camello {i + 1}" for i in range(num_camellos)] - # Limpiar la visualización anterior for widget in self.frame_progreso.winfo_children(): widget.destroy() self.progreso_labels = {} self.camellos = iniciar_carrera(nombres) - # --- MENSAJE SIMPLIFICADO --- self.carrera_estado_label.config(text=f"¡Carrera en marcha! 💨 ({num_camellos} participantes)") self.crear_visualizacion_carrera(nombres) @@ -199,7 +321,6 @@ class PanelCentral(ttk.Frame): if etiqueta_posicion and estado['posicion']: etiqueta_posicion.config(text=f"POS: {estado['posicion']} 🏆", foreground=COLOR_ACCION) - # --- MENSAJE SIMPLIFICADO --- self.carrera_estado_label.config(text="Carrera en curso...") 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 ''}", foreground=COLOR_ACCION) - # --- MENSAJE SIMPLIFICADO --- 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) @@ -253,7 +373,6 @@ class PanelCentral(ttk.Frame): if RESULTADO_ULTIMO and not RESULTADO_ULTIMO.get('activa', True) and self.frame_progreso: self.mostrar_resultado_final(RESULTADO_ULTIMO) else: - # --- MENSAJE SIMPLIFICADO AL CARGAR --- self.carrera_estado_label.config(text="Carrera lista para empezar.") 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) 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: self.after_cancel(self.after_id) self.after_id = None @@ -308,31 +427,31 @@ class PanelCentral(ttk.Frame): 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) # ------------------------------------------------------------- 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.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_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), + 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") - - # 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, font=('Arial', 10)) 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, 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.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) --- - frame_apps = ttk.LabelFrame(panel_chat, text="Minijuegos / Apps", padding=10, style='TFrame') - frame_apps.grid(row=9, 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") - # Botón 1: Placeholder - ttk.Button(frame_apps, text="App1 (T3-Red)", width=15, style='Action.TButton').pack(side="left", padx=5) + # 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)) - # Botón 2: Inicia la Carrera de Camellos (T2 Sincronización) - ttk.Button(frame_apps, - text="App2 (T2-Carrera 🏁)", - command=self.manejar_inicio_carrera, - width=15, - style='Action.TButton').pack(side="left", padx=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) - # Asegurar que las filas de abajo no se expandan - panel_chat.grid_rowconfigure(8, weight=0) - panel_chat.grid_rowconfigure(9, weight=0) \ No newline at end of file + # 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) + + panel_chat.grid_rowconfigure(8, weight=0) \ No newline at end of file diff --git a/vista/ventana_principal.py b/vista/ventana_principal.py index c4acc3f..f3dd9f1 100644 --- a/vista/ventana_principal.py +++ b/vista/ventana_principal.py @@ -16,7 +16,7 @@ class VentanaPrincipal(tk.Tk): def __init__(self): super().__init__() - self.title("Proyecto Integrado - PSP (Estilo Moderno Nativo)") + self.title("Proyecto Integrado - PSP") self.geometry("1200x800") self.label_reloj = None @@ -79,7 +79,7 @@ class VentanaPrincipal(tk.Tk): def crear_paneles_principales(self): """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) # Usando la constante importada