```
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:
parent
813b2b7d65
commit
983836d94c
16
Readme.md
16
Readme.md
|
|
@ -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)
|
||||||
|
|
@ -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.")
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue