feat(vista): Refactoriza ventana principal (main)

Refactoriza la ventana principal para modularizar y
mejorar la estructura.

* Reestructura 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:
BYolivia 2025-12-06 02:55:35 +01:00
parent 9a47fe104d
commit 731050242a
10 changed files with 398 additions and 295 deletions

View File

@ -9,21 +9,29 @@ def obtener_datos_cpu_ram():
:return: Diccionario con métricas.
"""
cpu_percent_total = psutil.cpu_percent(interval=None)
# --- 1. MEDICIÓN DE CPU (CORRECCIÓN CLAVE) ---
# La primera llamada 'primea' el cálculo de psutil.
psutil.cpu_percent(interval=None)
# La segunda llamada devuelve el porcentaje real calculado desde la primera.
# Aquí obtenemos el total y los núcleos en la misma llamada para consistencia.
cpu_percent_per_core = psutil.cpu_percent(interval=None, percpu=True)
cpu_percent_total = sum(cpu_percent_per_core) / len(cpu_percent_per_core)
# Alternativamente, psutil.cpu_percent(interval=None) - pero calculando la media es más seguro.
# --- 2. RESTO DE DATOS (Sin cambios) ---
mem = psutil.virtual_memory()
num_procesos = len(psutil.pids())
cpu_freq = psutil.cpu_freq()
datos = {
'cpu_total': cpu_percent_total,
'cpu_cores': cpu_percent_per_core,
'cpu_total': round(cpu_percent_total, 1),
'cpu_cores': [round(p, 1) for p in cpu_percent_per_core],
'ram_total_gb': round(mem.total / (1024 ** 3), 2),
'ram_uso_gb': round(mem.used / (1024 ** 3), 2),
'ram_percent': mem.percent,
# Nota: 'num_hilos' es técnicamente el número de PIDs (procesos), no hilos
'num_hilos': num_procesos,
'cpu_freq_mhz': cpu_freq.current if cpu_freq else 0
}

View File

@ -15,8 +15,6 @@ def actualizar_historial_datos(net_in_kb, net_out_kb, cpu_percent, ram_percent):
"""
Recopila los datos actuales de CPU, RAM y Red pasados como argumento a sus historiales.
"""
# 🎯 CORRECCIÓN: Los datos de CPU/RAM ahora vienen como argumentos, no se calculan aquí.
# 1. Añadir CPU y gestionar la longitud
historial_cpu.append(cpu_percent)
if len(historial_cpu) > MAX_PUNTOS:
@ -40,7 +38,7 @@ def crear_grafico_recursos(figure):
"""
Crea o actualiza un gráfico que muestre la evolución de CPU, RAM y Red.
"""
# Limpiar la figura antes de dibujar
# Limpiar la figura antes de dibujar (CRUCIAL para redibujo)
figure.clear()
# Configuramos el fondo de la figura para que coincida con el estilo de la aplicación
@ -50,7 +48,7 @@ def crear_grafico_recursos(figure):
# 3 filas para CPU, RAM, Red con espaciado vertical
gs = figure.add_gridspec(3, 1, hspace=0.6, top=0.95, bottom=0.05, left=0.1, right=0.95)
# --- Función Helper para el estilo btop ---
# --- Función Helper para el estilo ---
def configurar_ejes_historial(ax, title, color, data, y_limit=100, y_ticks=None):
ax.set_facecolor('#f0f0f0') # Fondo del área de dibujo
ax.set_title(title, fontsize=9, loc='left', pad=10)

View File

@ -21,24 +21,30 @@ class NetIOMonitor(threading.Thread):
# Almacenamiento seguro para los últimos datos de tráfico
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
self.data_in_kb = 0.0
self.data_out_kb = 0.0
self.cpu_percent = 0.0
self.ram_percent = 0.0
# Almacena el contador anterior para calcular la diferencia (tasa)
self.last_counters = psutil.net_io_counters()
# Necesario para inicializar la medición de CPU/RAM al inicio
# Inicializar la medición de CPU (sin intervalo) para la primera lectura.
# Esto 'primea' el cálculo de psutil para la primera vez que se llama en run().
psutil.cpu_percent(interval=None)
def run(self):
"""Método principal del hilo."""
try:
# Esperar el intervalo antes de la primera lectura para tener una base
# y que el cálculo de la tasa sea correcto desde el inicio.
time.sleep(self.intervalo)
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()
# Pausa al final, simplifica la lógica de _actualizar_datos
time.sleep(self.intervalo)
except Exception as e:
print(f"Error fatal en el hilo NetIOMonitor: {e}")
self._stop_event.set()
@ -52,8 +58,9 @@ class NetIOMonitor(threading.Thread):
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)
# 🎯 CORRECCIÓN: Llamar sin intervalo. psutil.cpu_percent() devolverá el uso
# desde la última llamada (que fue hace 'self.intervalo' segundos).
current_cpu = psutil.cpu_percent()
current_ram = psutil.virtual_memory().percent
# Calcular la diferencia de bytes recibidos y enviados desde la última lectura

View File

@ -30,19 +30,72 @@ class MusicReproductor:
self.instance = vlc.Instance()
self.player = self.instance.media_player_new()
self.current_media = None
self.is_playing = False
# 🔑 Variables de estado/control
self._current_url = None # Guarda la última URL cargada
self._is_playing = False # Indica si se está reproduciendo activamente (no pausado)
# Configurar volumen inicial
self.ajustar_volumen(initial_volume)
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.player.audio_get_volume()}")
self.set_volumen(initial_volume) # 🔑 Renombrado a set_volumen
print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.get_volumen()}")
def ajustar_volumen(self, valor_porcentual):
# -------------------------------------------------------------
# 🔑 MÉTODOS REQUERIDOS POR LA VISTA
# -------------------------------------------------------------
def get_volumen(self):
"""
Ajusta el volumen del reproductor (0 a 100).
[REQUERIDO] Devuelve el nivel de volumen actual (0-100).
Este método es esencial para inicializar el slider de la interfaz.
"""
# VLC proporciona el volumen actual directamente
return self.player.audio_get_volume()
def set_volumen(self, valor_porcentual):
"""
[REQUERIDO - Antes ajustar_volumen] 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 esta_reproduciendo(self):
"""
[REQUERIDO] Devuelve True si el reproductor está en estado Playing o Paused
y nosotros lo consideramos 'activo'.
Usaremos el estado interno _is_playing para indicar el estado activo/pausado.
"""
return self._is_playing
def continuar(self):
"""
[REQUERIDO] Reanuda la reproducción si está pausada, o inicia el stream si está detenido.
Devuelve True si la reproducción se inició/continuó.
"""
# Si está pausado, reanuda
if self.player.get_state() == vlc.State.Paused:
self.player.play()
self._is_playing = True
print("▶️ [VLC] Reproducción reanudada.")
return True
# Si está detenido y hay un medio cargado, intenta reproducir
elif self.player.get_state() == vlc.State.Stopped and self.current_media:
self.player.play()
self._is_playing = True
print("▶️ [VLC] Reproducción iniciada desde stream cargado.")
return True
# Si no hay medio cargado, no puede continuar
elif not self.current_media:
print(" [VLC] No hay stream cargado para continuar.")
return False
# Si ya está reproduciendo, lo ignoramos
return True
# -------------------------------------------------------------
# MÉTODOS DE CONTROL DE VLC
# -------------------------------------------------------------
def cargar_y_reproducir(self, url_stream):
"""
@ -50,29 +103,33 @@ class MusicReproductor:
"""
if not url_stream:
print("❌ [VLC] URL del stream vacía.")
return
return False, "URL del stream vacía."
print(f"🔄 [VLC] Intentando cargar y reproducir: {url_stream}")
# Detener la reproducción anterior
self.player.stop()
self.current_media = self.instance.media_new(url_stream)
self.player.set_media(self.current_media)
self._current_url = url_stream
# Iniciar reproducción
self.player.play()
self.is_playing = True
print("✅ [VLC] Reproducción iniciada.")
self._is_playing = True
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.")
# Esperar un poco para confirmar el estado de reproducción
# En entornos reales, se usaría un callback de evento para esto.
import time
time.sleep(0.1)
if self.player.get_state() in [vlc.State.Playing, vlc.State.Opening]:
print("✅ [VLC] Reproducción iniciada.")
return True, url_stream
else:
print(" [VLC] Ya está reproduciéndose o esperando un stream.")
print(f"❌ [VLC] Fallo al iniciar la reproducción. Estado: {self.player.get_state()}")
self._is_playing = False
return False, "Fallo al iniciar el stream (Revisa la URL)."
def pausar(self):
"""
@ -80,18 +137,20 @@ class MusicReproductor:
"""
if self.player.get_state() == vlc.State.Playing:
self.player.pause()
self.is_playing = False
self._is_playing = False
print("⏸️ [VLC] Reproducción pausada.")
return True
else:
print(" [VLC] No se puede pausar, el reproductor no está en estado de reproducción.")
return False
def detener(self):
"""
Detiene la reproducción y libera los recursos. Crucial al cerrar la aplicación.
Detiene la reproducción y el estado activo.
"""
if self.player:
self.player.stop()
# 🎯 Solo liberamos el reproductor. No eliminamos self.instance.
self.player.release()
self.player = None # Esto asegura que el player se recree si es necesario
print("⏹️ [VLC] Reproductor detenido y recursos liberados.")
self._is_playing = False
print("⏹️ [VLC] Reproductor detenido.")
return True
return False

View File

@ -5,51 +5,38 @@ 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")
# -------------------------------------------------------------
# ❌ ELIMINAMOS EL BLOQUE try/except CON LA SIMULACIÓN DE VLC
# Ya que esta clase no debe interactuar directamente con MusicReproductor.
# -------------------------------------------------------------
class RadioPanel(ttk.Frame):
"""
Panel de la pestaña Radios (T3).
Gestiona la selección de emisoras y los controles de reproducción.
Gestiona únicamente la selección de emisoras y DELEGA la reproducción
al ReproductorController en el Panel Lateral.
"""
NOMBRE_FICHERO_RADIOS = "res/radios.json"
def __init__(self, parent_notebook, root, *args, **kwargs):
def __init__(self, parent_notebook, root, reproductor_controller_instance=None, *args, **kwargs):
super().__init__(parent_notebook, *args, **kwargs)
self.root = root
# 🔑 REFERENCIA AL CONTROLADOR DE AUDIO DEL PANEL LATERAL
# Este controlador tiene los métodos cargar_stream() y manejar_stop().
self.reproductor_controller = reproductor_controller_instance
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())
# ❌ Se eliminaron: self.volumen_var y la inicialización de MusicReproductor.
self.crear_interfaz_radios(self)
# -------------------------------------------------------------
# 📻 VISTA Y LÓGICA DE RADIO
# 📻 LÓGICA DE DATOS
# -------------------------------------------------------------
def cargar_emisoras(self):
@ -64,8 +51,12 @@ class RadioPanel(ttk.Frame):
print(f"⚠️ Error al leer el archivo {self.NOMBRE_FICHERO_RADIOS}. Está mal formado.")
return []
# -------------------------------------------------------------
# 🖼️ VISTA (SOLO SELECCIONADOR)
# -------------------------------------------------------------
def crear_interfaz_radios(self, parent_frame):
"""Crea la interfaz para seleccionar la emisora de radio."""
"""Crea la interfaz para seleccionar la emisora de radio (SIN CONTROLES DE AUDIO)."""
frame_radio = ttk.Frame(parent_frame, padding=10, style='TFrame')
frame_radio.pack(expand=True, fill="both")
@ -99,25 +90,16 @@ class RadioPanel(ttk.Frame):
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)
# ❌ Se eliminaron los controles de volumen y Play/Pause/Stop.
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)
# -------------------------------------------------------------
# ⏯️ DELEGACIÓN DE LA LÓGICA DE AUDIO
# -------------------------------------------------------------
def seleccionar_radio(self, listbox):
"""Captura la selección y llama al reproductor para iniciar la reproducción."""
"""
Captura la selección y DELEGA la reproducción al ReproductorController.
"""
seleccion = listbox.curselection()
if seleccion:
indice = seleccion[0]
@ -126,22 +108,21 @@ class RadioPanel(ttk.Frame):
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()
# 🔑 DELEGACIÓN: Llamamos al controlador de audio del Panel Lateral
if self.reproductor_controller:
self.reproductor_controller.cargar_stream(url)
else:
# El error indica que falta conectar en panel_central.py
print("❌ Error: ReproductorController no está asignado.")
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)
# ❌ Se eliminaron los métodos controlar_reproduccion, cambiar_volumen.
def detener_actualizacion(self):
"""Método llamado por PanelCentral al cerrar la aplicación."""
if self.reproductor:
self.reproductor.detener()
"""Método llamado por PanelCentral al cerrar la aplicación (solo delega la detención)."""
# 🔑 DELEGACIÓN: El PanelCentral llama a esto al cerrar.
if self.reproductor_controller:
self.reproductor_controller.manejar_stop()
else:
# Si el controlador no existe, no hacemos nada, pero el PanelLateral debería manejar su propio cierre.
pass

View File

@ -2,9 +2,9 @@
import tkinter as tk
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # <-- ¡IMPORTACIÓN NECESARIA!
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
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í
from vista.config import *
class RecursosPanel(ttk.Frame):
@ -13,54 +13,56 @@ class RecursosPanel(ttk.Frame):
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
# Asegura que el frame del panel se expande
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.canvas = None
self.canvas_widget = None
self.grafico_frame = None
self.crear_interfaz_recursos(self)
# Estado inicial del gráfico
# Estado inicial del gráfico (limpia y configura la figura)
crear_grafico_recursos(self.figure)
# Después de la creación, nos aseguramos de que PanelCentral obtenga las referencias
# que espera si las necesita.
# ✅ DIBUJO INICIAL: Asegura que el gráfico vacío se muestre inmediatamente.
if self.canvas:
self.canvas.draw()
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')
# Ubicar el frame contenedor del gráfico
self.grafico_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
# Asegura que el contenido del gráfico_frame se expande
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
# CREAR Y UBICAR EL CANVAS
self.canvas = FigureCanvasTkAgg(self.figure, master=self.grafico_frame)
self.canvas_widget = self.canvas.get_tk_widget()
# Usamos .grid() para llenar el frame_contenedor
# ✅ CRÍTICO: El widget del canvas debe expandirse para llenar el gráfico_frame
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 actualizar_datos(self, net_in, net_out, cpu_percent, ram_percent):
"""Recibe los datos del monitor y actualiza el historial de datos en la lógica."""
# Recibe los 4 datos validados por la prueba de terminal
actualizar_historial_datos(net_in, net_out, cpu_percent, ram_percent)
def dibujar_grafico(self):
"""Llama a la función de redibujo del gráfico."""
"""Llama a la función de redibujo del gráfico y fuerza la actualización del canvas."""
if self.figure:
crear_grafico_recursos(self.figure)
# 1. Modifica la figura usando los datos del historial
crear_grafico_recursos(self.figure)
# 2. ✅ FORZAR EL REDIBUJADO DE TKINTER
if self.canvas:
self.canvas.draw()

View File

@ -5,6 +5,7 @@ from tkinter import ttk
from tkinter import messagebox
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import psutil # <--- IMPORTACIÓN REQUERIDA PARA CPU/RAM
# --- LÓGICA DE CONTROL UNIVERSAL ---
from logica.T1.trafficMeter import iniciar_monitor_red
@ -34,9 +35,12 @@ class PanelCentral(ttk.Frame):
INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS
def __init__(self, parent, root, *args, **kwargs):
# 🔑 CORRECCIÓN CRUCIAL: Añadir 'panel_lateral' al constructor para acceder al controlador de música.
def __init__(self, parent, root, panel_lateral, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.root = root
# 🔑 Guardamos la referencia para poder acceder al ReproductorController
self.panel_lateral = panel_lateral
# --- Variables de Estado y Lógica Central ---
self.after_id = None
@ -56,12 +60,10 @@ class PanelCentral(ttk.Frame):
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_columnconfigure(0, weight=1)
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
# -------------------------------------------------------------
@ -69,7 +71,7 @@ class PanelCentral(ttk.Frame):
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
frame_izquierdo.grid(row=0, column=0, sticky="nsew")
frame_izquierdo.grid_rowconfigure(0, weight=1)
frame_izquierdo.grid_columnconfigure(0, weight=1)
@ -123,6 +125,14 @@ class PanelCentral(ttk.Frame):
vista_instancia = AlarmaPanel(frame, self.root, self.alarm_manager)
self.modulos[sub_tab_text] = vista_instancia
# 🔑 CORRECCIÓN CLAVE: Inyectar la dependencia del controlador de música en RadioPanel
elif sub_tab_text == "Radios":
# Accedemos al controlador de música a través de la referencia al PanelLateral
reproductor_controller = getattr(self.panel_lateral, 'controles_musica', None)
vista_instancia = RadioPanel(frame, self.root,
reproductor_controller_instance=reproductor_controller)
self.modulos[sub_tab_text] = vista_instancia
else:
vista_instancia = ClasePanel(frame, self.root)
self.modulos[sub_tab_text] = vista_instancia
@ -197,22 +207,29 @@ class PanelCentral(ttk.Frame):
def actualizar_recursos(self):
"""Obtiene los datos del sistema y delega el dibujo al módulo RecursosPanel."""
try:
if self.net_monitor is None:
raise Exception("TrafficMeter no inicializado.")
if 'Recursos' not in self.modulos or self.net_monitor is None:
# Si el módulo Recursos o el monitor no están listos, reprogramamos.
self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos)
return
# 1. OBTENER DATOS (Red, CPU y RAM)
# 🔑 CORRECCIÓN CLAVE: Desempaquetar los 4 valores devueltos por TrafficMeter.
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()
# 2. ACTUALIZAR Y DIBUJAR
recursos_panel = self.modulos['Recursos']
if self.canvas:
self.canvas.draw()
# Llama a actualizar_datos en la lógica de graficos.py
recursos_panel.actualizar_datos(net_in, net_out, cpu_percent, ram_percent)
# Llama a dibujar_grafico en la vista (que ahora incluye self.canvas.draw())
recursos_panel.dibujar_grafico()
except Exception as e:
# Captura y muestra el error, pero no detiene la tarea
print(f"Error en la actualización de recursos T1: {e}")
pass
# 3. REPROGRAMAR TAREA (Crucial para el bucle)
self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos)
def iniciar_actualizacion_automatica(self):
@ -230,6 +247,12 @@ class PanelCentral(ttk.Frame):
return
print("Iniciando actualización automática de recursos.")
# 🔑 CORRECCIÓN: Realizar una llamada inicial a psutil.cpu_percent()
# para establecer el punto de partida del intervalo de medición en el hilo principal.
psutil.cpu_percent(interval=None)
# Iniciar el ciclo de actualización.
self.after_id = self.after(0, self.actualizar_recursos)
def detener_actualizacion_automatica(self):
@ -247,5 +270,4 @@ class PanelCentral(ttk.Frame):
# 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()
modulo.detener_actualizacion()

View File

@ -5,16 +5,18 @@ 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 LÓGICA T2
from logica.T2.musicReproductor import MusicReproductor
# --- 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
# ❌ Eliminamos: from vista.central_panel.view_radio import RadioPanel
# 🔑 NUEVA IMPORTACIÓN DE VISTA MODULAR
from vista.reproductor_controller import ReproductorController
from vista.config import *
@ -33,9 +35,12 @@ class PanelLateral(ttk.Frame):
self.root = root
self.panel_central = panel_central
self.controles_musica = None
self.entrada_superior = None
# 🔑 INSTANCIA DE LÓGICA DE MÚSICA T2
# Inicializamos el objeto de la lógica de reproducción aquí
self.music_reproductor = MusicReproductor()
self.configurar_estilos_locales(root)
# Configuración de Layout Principal
@ -52,6 +57,7 @@ class PanelLateral(ttk.Frame):
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")
# 🔑 LLAMADA AL NUEVO CONTROLADOR
self.crear_controles_musica() # Fila 100
# -------------------------------------------------------------
@ -82,7 +88,7 @@ class PanelLateral(ttk.Frame):
acciones_aplicaciones = [
("Visual Code", abrir_vscode),
("App2 (Carrera 🏁)", app2_comando),
("Carrera 🏁", app2_comando),
("App3", lambda: accion_placeholder("App3"))
]
self._crear_bloque_botones(self, titulo="Aplicaciones", acciones=acciones_aplicaciones, grid_row=2)
@ -95,13 +101,17 @@ class PanelLateral(ttk.Frame):
self._crear_bloque_botones(self, titulo="Procesos batch", acciones=acciones_batch, grid_row=3)
def crear_controles_musica(self):
"""Crea el área para alojar los controles de música/radio."""
"""Crea el área para alojar los controles de música/radio usando el nuevo controlador."""
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)
# Instancia la clase RadioPanel, que ahora contiene solo Play/Pause y Volumen
self.controles_musica = RadioPanel(frame_musica, self.root)
# 🔑 REEMPLAZO CLAVE: Usamos ReproductorController y le pasamos la instancia de la lógica.
self.controles_musica = ReproductorController(
frame_musica,
self.root,
music_reproductor_instance=self.music_reproductor # Pasamos la instancia de la lógica T2
)
self.controles_musica.grid(row=0, column=0, sticky="nsew")
frame_musica.grid_rowconfigure(0, weight=1)
@ -199,4 +209,12 @@ class PanelLateral(ttk.Frame):
ttk.Label(frame_titulo, text=titulo, font=FUENTE_NEGOCIOS).pack(anchor="w", padx=5)
for texto_boton, comando in acciones:
ttk.Button(frame_seccion, 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)
def set_panel_central_reference(self, panel_central_instance):
"""
Asigna la referencia al PanelCentral una vez que ambos paneles han sido inicializados.
Esto resuelve la dependencia circular.
"""
self.panel_central = panel_central_instance
print("✅ [PanelLateral] Referencia a Panel Central establecida.")

View File

@ -0,0 +1,124 @@
# Módulo: vista/reproductor_controller.py
import tkinter as tk
from tkinter import ttk
# ⚠️ Asegúrate de que esta ruta es correcta: 'vista/config' o 'config'
from vista.config import *
class ReproductorController(ttk.Frame):
"""
Controlador de la interfaz de reproducción de música (Panel Lateral).
Delega todas las acciones de audio a la instancia MusicReproductor.
"""
def __init__(self, parent, root, music_reproductor_instance=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.root = root
self.reproductor = music_reproductor_instance # Instancia de MusicReproductor (Lógica T2)
# Variables de control de UI
# Intentar obtener el volumen inicial de la lógica, si está disponible
initial_volume = self.reproductor.get_volumen() if self.reproductor and hasattr(self.reproductor,
'get_volumen') else 50
self.volumen_var = tk.DoubleVar(value=initial_volume)
# Estado inicial del botón Play/Pause
self.play_pause_text = tk.StringVar(value="▶️")
if self.reproductor and hasattr(self.reproductor, 'esta_reproduciendo') and self.reproductor.esta_reproduciendo():
self.play_pause_text.set("⏸️")
# Configuración de Layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
self.crear_controles()
def crear_controles(self):
"""Crea el marco con el slider de volumen y los botones de control."""
main_frame = ttk.Frame(self, padding=10, style='TFrame')
main_frame.grid(row=0, column=0, sticky="nsew")
main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_columnconfigure(1, weight=1)
# --- Título ---
ttk.Label(main_frame, text="🎵 Control de Música", font=FUENTE_NEGOCIOS).grid(
row=0, column=0, columnspan=2, pady=(0, 10), sticky="w")
# --- Slider de Volumen ---
ttk.Label(main_frame, text="Volumen:", font=FUENTE_NORMAL).grid(
row=1, column=0, columnspan=2, pady=(5, 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=2, column=0, columnspan=2, sticky="ew", pady=(5, 10))
# --- Botones de Control ---
self.boton_play_pause = ttk.Button(
main_frame,
textvariable=self.play_pause_text,
style='Action.TButton',
command=self.manejar_play_pause
)
self.boton_play_pause.grid(row=3, column=0, sticky="ew", padx=(0, 5), pady=5)
self.boton_stop = ttk.Button(main_frame, text="⏹️", style='Action.TButton', command=self.manejar_stop)
self.boton_stop.grid(row=3, column=1, sticky="ew", padx=(5, 0), pady=5)
def manejar_play_pause(self):
"""Alterna entre reproducir y pausar llamando al reproductor de la lógica."""
if not self.reproductor: return
if self.reproductor.esta_reproduciendo():
self.reproductor.pausar()
self.play_pause_text.set("▶️")
else:
# Llama a continuar, que intenta reanudar o iniciar el último medio
if self.reproductor.continuar():
self.play_pause_text.set("⏸️")
else:
self.play_pause_text.set("▶️")
def manejar_stop(self):
"""Detiene completamente la reproducción."""
if not self.reproductor: return
self.reproductor.detener()
self.play_pause_text.set("▶️")
def manejar_ajuste_volumen(self, valor):
"""Ajusta el volumen del reproductor basado en el slider."""
if not self.reproductor: return
volumen = int(float(valor))
# 🔑 Delega el ajuste de volumen a la lógica (MusicReproductor.set_volumen)
self.reproductor.set_volumen(volumen)
# -------------------------------------------------------------
# 📡 FUNCIÓN EXPORTADA PARA RECIBIR EL STREAM DEL view_radio
# -------------------------------------------------------------
def cargar_stream(self, url_stream):
"""
Recibe la URL de la radio seleccionada y la pasa a la lógica para su reproducción.
"""
if not self.reproductor:
print("Error: Instancia de reproductor no asignada.")
return
# 🔑 Llama a la lógica para detener lo anterior, cargar y reproducir el nuevo stream
success, message = self.reproductor.cargar_y_reproducir(url_stream)
# Actualiza la UI basada en el resultado de la carga
if success:
self.play_pause_text.set("⏸️")
else:
self.play_pause_text.set("▶️")
print(f"⚠️ Fallo en la carga del stream: {message}")

View File

@ -24,7 +24,8 @@ 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
self.panel_central = None
self.panel_lateral = None # 🔑 Añadir inicialización a None
style = ttk.Style()
style.theme_use('clam')
@ -48,6 +49,9 @@ class VentanaPrincipal(tk.Tk):
self.iniciar_actualizacion_reloj()
self.iniciar_actualizacion_clima()
# 🔑 CORRECCIÓN CRÍTICA: Iniciar el bucle de actualización del Panel Central (T1)
self.iniciar_actualizacion_graficos()
def configurar_estilos(self, s: ttk.Style):
"""Define estilos visuales personalizados."""
@ -77,20 +81,37 @@ class VentanaPrincipal(tk.Tk):
s.configure('TNotebook.Tab', padding=[10, 5], font=FUENTE_NEGOCIOS)
s.map('TNotebook.Tab', background=[('selected', COLOR_FONDO)], foreground=[('selected', COLOR_ACCION)])
# 🔑 MÉTODO CORREGIDO
def crear_paneles_principales(self):
"""Ensambla el panel lateral y el panel central."""
"""Ensambla el panel lateral y el panel central, resolviendo la dependencia circular."""
# Panel Central debe inicializarse primero para pasar la referencia al lateral
# self es parent
self.panel_central = PanelCentral(self, self)
# 1. Crear Panel Lateral: Inicialmente no necesita el PanelCentral, solo necesita saber que existirá.
# PanelLateral espera: PanelLateral(parent, root, panel_central_ref, ...)
# Usamos None para 'panel_central_ref' por ahora.
self.panel_lateral = PanelLateral(self, self, None, 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)
# 2. Crear Panel Central: Ahora sí necesita la referencia al PanelLateral para inyectar el controlador de audio.
# PanelCentral espera: PanelCentral(parent, root, panel_lateral)
self.panel_central = PanelCentral(self, self, panel_lateral=self.panel_lateral)
self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10)
# 🎯 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)
# 3. Finalizar la dependencia circular: Asignar la referencia del Panel Central al Panel Lateral.
# El PanelLateral requiere la referencia de PanelCentral para manejar eventos (ej. Scrapping).
self.panel_lateral.set_panel_central_reference(self.panel_central)
self.panel_lateral.grid_propagate(False)
# --- LÓGICA DE ACTUALIZACIÓN DE GRÁFICOS (T1) ---
def iniciar_actualizacion_graficos(self):
"""Inicia el ciclo de actualización de recursos del PanelCentral."""
if self.panel_central:
print("Iniciando actualización automática de gráficos del Panel Central.")
# Llama al método que inicializa el TrafficMeter y el self.after()
self.panel_central.iniciar_actualizacion_automatica()
else:
print("Error: Panel Central no inicializado para iniciar gráficos.")
# --- LÓGICA DE ACTUALIZACIÓN DE RELOJ ---
def actualizar_reloj(self):
@ -103,9 +124,6 @@ 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}")
# CORRECCIÓN: Eliminamos la detención para que el after continúe intentando
# self.detener_actualizacion_reloj()
# return
self.reloj_after_id = self.after(INTERVALO_RELOJ_MS, self.actualizar_reloj)
@ -132,9 +150,6 @@ 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}")
# CORRECCIÓN: Eliminamos la detención para que el after continúe intentando
# self.detener_actualizacion_clima()
# return
self.clima_after_id = self.after(INTERVALO_CLIMA_MS, self.actualizar_clima)
@ -156,7 +171,7 @@ class VentanaPrincipal(tk.Tk):
self.detener_actualizacion_reloj()
self.detener_actualizacion_clima()
# Solo intenta detener el panel central si fue inicializado
# Detiene el hilo de TrafficMeter y el ciclo de repintado del gráfico
if self.panel_central:
self.panel_central.detener_actualizacion_automatica()
@ -188,135 +203,4 @@ 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")# 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}")
self.label_reloj.pack(side="left")