diff --git a/ProyectoAdrianHustea/Proyecto.py b/ProyectoAdrianHustea/Proyecto.py new file mode 100644 index 0000000..61f3eb0 --- /dev/null +++ b/ProyectoAdrianHustea/Proyecto.py @@ -0,0 +1,1362 @@ +import tkinter as tk +from tkinter import Menu, filedialog, simpledialog, messagebox +from tkinter import ttk +import threading +import time +import datetime +import webbrowser +import subprocess +import os +import collections +import random +import urllib.request +import re +import urllib.error + +# --- Importar y configurar Pygame para la música/alarma --- +try: + import pygame + # Intentar inicializar Pygame Mixer. Si falla, deshabilitar la música. + try: + pygame.mixer.init() + MUSIC_AVAILABLE = True + except pygame.error as e: + MUSIC_AVAILABLE = False + print(f"Error al inicializar pygame mixer: {e}. Funcionalidad de audio deshabilitada.") + + MUSIC_FILE = "music.mp3" # Archivo música de fondo + ALARM_FILE = "alarm.mp3" # <--- ¡NUEVO ARCHIVO DE ALARMA! + +except ImportError: + MUSIC_AVAILABLE = False + print("Pygame no está instalado. La funcionalidad de audio no estará disponible.") + + +# --- Importación específica para el sonido de alarma (Solo Windows) --- +try: + import winsound + WINSOUND_AVAILABLE = True +except ImportError: + WINSOUND_AVAILABLE = False +# --------------------------------------------------------------------- + +# --- IMPORTACIONES PARA GRÁFICOS DE RECURSOS --- +import psutil +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +# ----------------------------------------------- + +# --- HILOS GLOBALES PARA CONTROL --- +cpu_monitor = None +memory_monitor = None +disk_monitor = None +net_monitor = None +network_thread = None +clock_thread = None +status_bar_time_thread = None +alarm_stop_event = threading.Event() +countdown_after_id = None +# --- VARIABLES GLOBALES DEL JUEGO DE COCHES --- +race_threads = [] +race_lock = threading.Lock() +winner = None +race_stop_event = threading.Event() +# --- VARIABLES GLOBALES DE MÚSICA --- +music_state = False # False = parada, True = sonando +# --------------------------------------------- + + +# --- CLASE DE GESTIÓN DE LA BARRA DE ESTADO --- +class StatusBarManager: + """Gestiona los tres labels dinámicos de la barra inferior (más los dos nuevos).""" + def __init__(self, parent_frame, root): + self.parent_frame = parent_frame + self.root = root + self.labels = {} + self._initialize_labels() + + def _initialize_labels(self): + label_configs = [ + ("correos", "Correos sin leer (0)", "yellow"), + ("temperatura", "Temperatura local (0°C)", "orange"), + ("status_1", "Estado 1 (Libre)", "green"), + ("status_2", "Estado 2 (Listo)", "blue"), + ("status_3", "Estado 3", "cyan"), + ] + + for name, initial_text, bg_color in label_configs[2:]: + label = tk.Label( + self.parent_frame, + text=initial_text, + bg=bg_color, + anchor="w", + width=20, + bd=1, + relief="sunken" + ) + label.pack(side="left", fill="x", expand=True) + self.labels[name] = label + + for name, initial_text, bg_color in label_configs[:2]: + label = tk.Label( + self.parent_frame, + text=initial_text, + bg=bg_color, + anchor="w", + width=25, + bd=1, + relief="flat", + font=("Helvetica", 10) + ) + label.pack(side="right", fill="x", padx=5) + self.labels[name] = label + + + def set_status(self, label_name, text, bg_color=None): + if label_name in self.labels: + label = self.labels[label_name] + config = {"text": text} + if bg_color: + config["bg"] = bg_color + + self.root.after(0, label.config, config) + else: + print(f"Error: Label '{label_name}' no encontrado.") + + +# --- CLASE PARA MONITORIZAR Y GRAFICAR RECURSOS --- +class SystemMonitor: + def __init__(self, master, resource_type, width=4, height=3): + self.master = master + self.resource_type = resource_type + self.update_interval = 1000 + + if resource_type == 'net': + self.data_history_sent = collections.deque([0] * 30, maxlen=30) + self.data_history_recv = collections.deque([0] * 30, maxlen=30) + self._last_net_io = psutil.net_io_counters() + else: + self.data_history = collections.deque([0] * 30, maxlen=30) + + self.fig, self.ax = plt.subplots(figsize=(width, height), dpi=100) + + self.canvas = FigureCanvasTkAgg(self.fig, master=master) + self.canvas_widget = self.canvas.get_tk_widget() + self.canvas_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.setup_plot() + + self.stop_event = threading.Event() + self.monitor_thread = threading.Thread(target=self._data_collection_loop, daemon=True) + self.monitor_thread.start() + + def setup_plot(self): + title_map = {'cpu': "Uso de CPU", 'memory': "Uso de RAM", 'disk': "Uso de Disco (I/O)", 'net': "Tasa de Red (Kb/s)"} + ylabel_map = {'cpu': "% Uso", 'memory': "% Uso", 'disk': "% Disco", 'net': "Kb/s"} + + self.ax.set_title(title_map.get(self.resource_type, "Recurso"), fontsize=10) + self.ax.set_ylim(0, 100 if self.resource_type != 'net' else 500) + self.ax.set_ylabel(ylabel_map.get(self.resource_type, "Valor"), fontsize=8) + self.ax.set_xlabel("Tiempo (s)", fontsize=8) + self.ax.tick_params(axis='both', which='major', labelsize=7) + + if self.resource_type == 'net': + self.line_sent, = self.ax.plot(self.data_history_sent, label='Enviado (Kb)', color='red') + self.line_recv, = self.ax.plot(self.data_history_recv, label='Recibido (Kb)', color='blue') + self.ax.legend(loc='upper right', fontsize=7) + else: + self.line, = self.ax.plot(self.data_history, label=f'{self.resource_type.upper()} %', color='blue') + self.ax.legend(loc='upper right', fontsize=7) + + self.fig.tight_layout(pad=1.0) + + def _get_data(self): + if self.resource_type == 'cpu': + return psutil.cpu_percent(interval=0.1) + elif self.resource_type == 'memory': + return psutil.virtual_memory().percent + elif self.resource_type == 'disk': + return psutil.disk_usage('/').percent + elif self.resource_type == 'net': + current_net_io = psutil.net_io_counters() + time_diff = self.update_interval / 1000.0 + + bytes_sent = current_net_io.bytes_sent - self._last_net_io.bytes_sent + bytes_recv = current_net_io.bytes_recv - self._last_net_io.bytes_recv + + kbs_sent = (bytes_sent / 1024) / time_diff + kbs_recv = (bytes_recv / 1024) / time_diff + + self._last_net_io = current_net_io + return kbs_sent, kbs_recv + return 0 + + def _data_collection_loop(self): + while not self.stop_event.is_set(): + data_point = self._get_data() + + if self.resource_type == 'net': + kbs_sent, kbs_recv = data_point + self.data_history_sent.append(kbs_sent) + self.data_history_recv.append(kbs_recv) + else: + self.data_history.append(data_point) + + self.master.after(0, self._update_gui) + + time.sleep(self.update_interval / 1000.0) + + def _update_gui(self): + + if self.resource_type == 'net': + self.line_sent.set_ydata(self.data_history_sent) + self.line_recv.set_ydata(self.data_history_recv) + + latest_sent = self.data_history_sent[-1] + latest_recv = self.data_history_recv[-1] + self.ax.set_title(f"Tasa de Red: S: {latest_sent:.1f} Kb/s, R: {latest_recv:.1f} Kb/s", fontsize=10) + else: + self.line.set_ydata(self.data_history) + latest_value = self.data_history[-1] + self.ax.set_title(f"Uso de {self.resource_type.upper()}: {latest_value:.1f}%", fontsize=10) + + self.canvas.draw_idle() + + def stop(self): + self.stop_event.set() + + +# --- GESTOR DE HILO DE RED --- +class NetworkTrafficThread: + """Hilo que monitorea y actualiza el tráfico total de red.""" + def __init__(self, root, label_in, label_out): + self.root = root + self.label_in = label_in + self.label_out = label_out + self.update_interval = 1000 # 1 segundo + self.stop_event = threading.Event() + + self.initial_net_io = psutil.net_io_counters() + + self.thread = threading.Thread(target=self._monitor_loop, daemon=True) + self.thread.start() + + def _monitor_loop(self): + while not self.stop_event.is_set(): + current_net_io = psutil.net_io_counters() + + bytes_sent_total = current_net_io.bytes_sent - self.initial_net_io.bytes_sent + bytes_recv_total = current_net_io.bytes_recv - self.initial_net_io.bytes_recv + + kb_sent_total = bytes_sent_total / 1024 + kb_recv_total = bytes_recv_total / 1024 + + self.root.after(0, self._update_gui, kb_sent_total, kb_recv_total) + + time.sleep(self.update_interval / 1000.0) + + def _update_gui(self, kb_sent, kb_recv): + self.label_in.config(text=f"KB Recibidos (Input): {kb_recv:,.2f} KB") + self.label_out.config(text=f"KB Enviados (Output): {kb_sent:,.2f} KB") + + def stop(self): + self.stop_event.set() + + +# --- FUNCIONES DE MÚSICA --- + +def initialize_music(): + """Carga el archivo de música. Llamar una vez al inicio o al cambiar a T2.""" + global MUSIC_AVAILABLE + + # Solo intentamos cargar si Pygame está disponible y si no hay música ya cargada + if MUSIC_AVAILABLE and not pygame.mixer.music.get_busy() and pygame.mixer.get_init(): + try: + pygame.mixer.music.load(MUSIC_FILE) + status_manager.set_status("status_3", f"Música cargada: {MUSIC_FILE}", "purple") + except pygame.error as e: + messagebox.showerror("Error de Audio", f"No se pudo cargar el archivo '{MUSIC_FILE}'. Asegúrate de que existe y es compatible. Error: {e}") + MUSIC_AVAILABLE = False + status_manager.set_status("status_3", "Error al cargar música", "magenta") + + +def toggle_music(button): + """Inicia o detiene la reproducción de música en bucle.""" + global music_state, MUSIC_AVAILABLE + + if not MUSIC_AVAILABLE: + messagebox.showwarning("Advertencia", "La funcionalidad de música no está disponible (Pygame o archivo no encontrado).") + return + + if not music_state: + # Asegurarse de que el archivo esté cargado antes de intentar reproducir + if pygame.mixer.music.get_pos() == 0 and not pygame.mixer.music.get_busy(): + initialize_music() + if not MUSIC_AVAILABLE: + return + + # 1. Iniciar la música + try: + # -1 para reproducir en bucle (infinitamente) + pygame.mixer.music.play(-1) + music_state = True + button.config(text="STOP MÚSICA (En Bucle)", style="Red.TButton") + status_manager.set_status("status_3", "Música sonando en bucle", "purple") + except pygame.error as e: + messagebox.showerror("Error de Reproducción", f"Error al reproducir: {e}") + MUSIC_AVAILABLE = False + else: + # 2. Detener la música + pygame.mixer.music.stop() + music_state = False + button.config(text="MÚSICA (Play)", style="Green.TButton") + status_manager.set_status("status_3", "Música detenida", "cyan") + +def stop_music_clean(): + """Detiene la música de forma limpia al salir de la app o cambiar de pestaña.""" + global music_state, MUSIC_AVAILABLE + if MUSIC_AVAILABLE and music_state: + pygame.mixer.music.stop() + music_state = False + +# --- FIN DE FUNCIONES DE MÚSICA --- + + +# --- FUNCIONES PARA LA PESTAÑA T2: SCRAPING (Implementación Books To Scrape) --- + +def run_simple_scraper(label_resultado): + """ + Inicia el scraping de la página de práctica de libros (books.toscrape.com). + """ + URL = "http://books.toscrape.com/" + status_manager.set_status("status_3", "Iniciando scraping de libros...", "orange") + label_resultado.config(text="Buscando títulos...", fg="orange") + + # 2. Hilo para no bloquear la GUI + threading.Thread(target=_scrape_thread, args=(URL, label_resultado), daemon=True).start() + +def _scrape_thread(URL, label_resultado): + """Ejecuta la lógica de scraping de libros dentro de un hilo.""" + global root, status_manager + + # User-Agent no es estrictamente necesario aquí, pero es una buena práctica + USER_AGENT = 'Mozilla/5.0' + req = urllib.request.Request(URL, headers={'User-Agent': USER_AGENT}) + + try: + # 1. Obtener la página + with urllib.request.urlopen(req, timeout=10) as response: + html = response.read().decode('utf-8') + + # 2. Buscamos todos los títulos de libros. + # El patrón busca la etiqueta HTML: title="[TÍTULO DEL LIBRO]" + title_matches = re.findall(r'title="(.*?)"', html) + + # Filtramos los títulos que no son de libros, buscando títulos largos + # Esto es una heurística; el primer libro tiene un título corto ("A Light in the Attic") + book_titles = [t for t in title_matches if len(t) > 30 and t != "A Light in the Attic"] + + if book_titles: + resultado_text = "✅ **Títulos de Libros Encontrados (books.toscrape.com):**\n\n" + for i, title in enumerate(book_titles[:20]): # Limitar a 20 resultados + resultado_text += f"[{i+1:02d}] {title}\n" + + color = "green" + status_text = f"Scraping OK: {len(book_titles)} títulos encontrados" + else: + resultado_text = "⚠️ Advertencia: No se pudo encontrar el patrón o el HTML ha cambiado." + color = "blue" + status_text = "Scraping parcial/patrón fallido" + + except urllib.error.HTTPError as e: + resultado_text = f"❌ Error HTTP: El servidor denegó el acceso (Error {e.code})." + color = "red" + status_text = "Error HTTP (4xx/5xx)" + except Exception as e: + resultado_text = f"❌ Error de Conexión/Scraping: {e}" + color = "red" + status_text = "Error de Red/Otro" + + # 4. Actualizar la GUI desde el hilo principal + root.after(0, label_resultado.config, {"text": resultado_text, "fg": color, "font": ("Consolas", 10)}) + status_manager.set_status("status_3", status_text, color) + + +def setup_scraping_tab(tab_frame): + """Configura la interfaz para la pestaña de Scraping.""" + for widget in tab_frame.winfo_children(): + widget.destroy() + + main_frame = tk.Frame(tab_frame) + main_frame.pack(expand=True, padx=20, pady=20, fill="both") + + tk.Label(main_frame, text="Web Scraping de Práctica (Books to Scrape)", font=("Arial", 16, "bold")).pack(pady=10) + + tk.Label( + main_frame, + text="Esto ejecuta un hilo que descarga la página de prueba 'books.toscrape.com' y extrae los 20 primeros títulos.", + wraplength=500, + justify="left" + ).pack(pady=5) + + # Etiqueta para mostrar el resultado + label_resultado = tk.Label( + main_frame, + text="Pulse el botón 'Scrapear' para comenzar...", + font=("Consolas", 10), + fg="gray", + justify="left" + ) + label_resultado.pack(pady=20, padx=10) + + # Botón de ejecución + ttk.Button( + main_frame, + text="SCRAPEAR LIBROS DE PRÁCTICA", + style="Green.TButton", + command=lambda: run_simple_scraper(label_resultado) + ).pack(pady=15) + +# --- FIN DE FUNCIONES DE SCRAPING --- + + +# --- FUNCIONES DE LA GUI GENERAL --- +def update_time(status_bar, stop_event): + """Bucle que actualiza la fecha y hora en la barra de estado.""" + while not stop_event.is_set(): + now = datetime.datetime.now() + day_of_week = now.strftime("%A") + time_str = now.strftime("%H:%M:%S") + date_str = now.strftime("%Y-%m-%d") + label_text = f"{day_of_week}, {date_str} - {time_str}" + status_bar.after(1000, status_bar.config, {"text": label_text}) + time.sleep(1) + +def stop_status_bar_time_thread(): + """Detiene el hilo de la hora de la barra de estado.""" + global status_bar_time_thread + if status_bar_time_thread and hasattr(status_bar_time_thread, 'stop_event'): + status_bar_time_thread.stop_event.set() + status_bar_time_thread = None + + +def open_external_url(url): + status_manager.set_status("status_2", f"Abriendo URL...", "orange") + webbrowser.open(url) + root.after(2000, lambda: status_manager.set_status("status_2", "URL lanzada.", "blue")) + +def run_bash_backup(): + def execute_script(): + status_manager.set_status("status_3", "Copia en curso (Bash)...", "red") + script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup_script.sh") + command = [script_path] + + try: + subprocess.run(command, capture_output=True, text=True, check=True) + status_manager.set_status("status_3", "Copia OK: ¡Éxito!", "lime") + except subprocess.CalledProcessError as e: + status_manager.set_status("status_3", f"Copia ¡FALLO! Código: {e.returncode}", "magenta") + except FileNotFoundError: + status_manager.set_status("status_3", "Error: Script no encontrado", "black") + + root.after(5000, lambda: status_manager.set_status("status_3", "Esperando tarea", "cyan")) + + threading.Thread(target=execute_script, daemon=True).start() + +def save_text_file(text_widget): + text_content = text_widget.get("1.0", "end-1c") + + file_path = filedialog.asksaveasfilename( + defaultextension=".txt", + filetypes=[ + ("Archivos de Texto", "*.txt"), + ("Todos los archivos", "*.*") + ], + title="Guardar documento del editor" + ) + + if file_path: + try: + with open(file_path, "w", encoding="utf-8") as file: + file.write(text_content) + + status_manager.set_status("status_3", f"Archivo guardado: {os.path.basename(file_path)}", "lime") + root.after(3000, lambda: status_manager.set_status("status_3", "Estado 3", "cyan")) + + except Exception as e: + status_manager.set_status("status_3", f"Error al guardar: {e}", "magenta") + root.after(3000, lambda: status_manager.set_status("status_3", "Estado 3", "cyan")) + + +# --- CONTROL DE HILOS Y PESTAÑAS T1 --- +class SystemMonitor: + def __init__(self, master, resource_type, width=4, height=3): + self.master = master + self.resource_type = resource_type + self.update_interval = 1000 + + if resource_type == 'net': + self.data_history_sent = collections.deque([0] * 30, maxlen=30) + self.data_history_recv = collections.deque([0] * 30, maxlen=30) + self._last_net_io = psutil.net_io_counters() + else: + self.data_history = collections.deque([0] * 30, maxlen=30) + + self.fig, self.ax = plt.subplots(figsize=(width, height), dpi=100) + + self.canvas = FigureCanvasTkAgg(self.fig, master=master) + self.canvas_widget = self.canvas.get_tk_widget() + self.canvas_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.setup_plot() + + self.stop_event = threading.Event() + self.monitor_thread = threading.Thread(target=self._data_collection_loop, daemon=True) + self.monitor_thread.start() + + def setup_plot(self): + title_map = {'cpu': "Uso de CPU", 'memory': "Uso de RAM", 'disk': "Uso de Disco (I/O)", 'net': "Tasa de Red (Kb/s)"} + ylabel_map = {'cpu': "% Uso", 'memory': "% Uso", 'disk': "% Disco", 'net': "Kb/s"} + + self.ax.set_title(title_map.get(self.resource_type, "Recurso"), fontsize=10) + self.ax.set_ylim(0, 100 if self.resource_type != 'net' else 500) + self.ax.set_ylabel(ylabel_map.get(self.resource_type, "Valor"), fontsize=8) + self.ax.set_xlabel("Tiempo (s)", fontsize=8) + self.ax.tick_params(axis='both', which='major', labelsize=7) + + if self.resource_type == 'net': + self.line_sent, = self.ax.plot(self.data_history_sent, label='Enviado (Kb)', color='red') + self.line_recv, = self.ax.plot(self.data_history_recv, label='Recibido (Kb)', color='blue') + self.ax.legend(loc='upper right', fontsize=7) + else: + self.line, = self.ax.plot(self.data_history, label=f'{self.resource_type.upper()} %', color='blue') + self.ax.legend(loc='upper right', fontsize=7) + + self.fig.tight_layout(pad=1.0) + + def _get_data(self): + if self.resource_type == 'cpu': + return psutil.cpu_percent(interval=0.1) + elif self.resource_type == 'memory': + return psutil.virtual_memory().percent + elif self.resource_type == 'disk': + return psutil.disk_usage('/').percent + elif self.resource_type == 'net': + current_net_io = psutil.net_io_counters() + time_diff = self.update_interval / 1000.0 + + bytes_sent = current_net_io.bytes_sent - self._last_net_io.bytes_sent + bytes_recv = current_net_io.bytes_recv - self._last_net_io.bytes_recv + + kbs_sent = (bytes_sent / 1024) / time_diff + kbs_recv = (bytes_recv / 1024) / time_diff + + self._last_net_io = current_net_io + return kbs_sent, kbs_recv + return 0 + + def _data_collection_loop(self): + while not self.stop_event.is_set(): + data_point = self._get_data() + + if self.resource_type == 'net': + kbs_sent, kbs_recv = data_point + self.data_history_sent.append(kbs_sent) + self.data_history_recv.append(kbs_recv) + else: + self.data_history.append(data_point) + + self.master.after(0, self._update_gui) + + time.sleep(self.update_interval / 1000.0) + + def _update_gui(self): + + if self.resource_type == 'net': + self.line_sent.set_ydata(self.data_history_sent) + self.line_recv.set_ydata(self.data_history_recv) + + latest_sent = self.data_history_sent[-1] + latest_recv = self.data_history_recv[-1] + self.ax.set_title(f"Tasa de Red: S: {latest_sent:.1f} Kb/s, R: {latest_recv:.1f} Kb/s", fontsize=10) + else: + self.line.set_ydata(self.data_history) + latest_value = self.data_history[-1] + self.ax.set_title(f"Uso de {self.resource_type.upper()}: {latest_value:.1f}%", fontsize=10) + + self.canvas.draw_idle() + + def stop(self): + self.stop_event.set() + +class NetworkTrafficThread: + """Hilo que monitorea y actualiza el tráfico total de red.""" + def __init__(self, root, label_in, label_out): + self.root = root + self.label_in = label_in + self.label_out = label_out + self.update_interval = 1000 # 1 segundo + self.stop_event = threading.Event() + + self.initial_net_io = psutil.net_io_counters() + + self.thread = threading.Thread(target=self._monitor_loop, daemon=True) + self.thread.start() + + def _monitor_loop(self): + while not self.stop_event.is_set(): + current_net_io = psutil.net_io_counters() + + bytes_sent_total = current_net_io.bytes_sent - self.initial_net_io.bytes_sent + bytes_recv_total = current_net_io.bytes_recv - self.initial_net_io.bytes_recv + + kb_sent_total = bytes_sent_total / 1024 + kb_recv_total = bytes_recv_total / 1024 + + self.root.after(0, self._update_gui, kb_sent_total, kb_recv_total) + + time.sleep(self.update_interval / 1000.0) + + def _update_gui(self, kb_sent, kb_recv): + self.label_in.config(text=f"KB Recibidos (Input): {kb_recv:,.2f} KB") + self.label_out.config(text=f"KB Enviados (Output): {kb_sent:,.2f} KB") + + def stop(self): + self.stop_event.set() + + +def setup_monitors_tab(tab_frame): + tk.Label(tab_frame, text="Monitorización de Recursos del Sistema", font=("Arial", 14, "bold")).pack(pady=10) + + monitor_grid_frame = tk.Frame(tab_frame) + monitor_grid_frame.pack(fill="both", expand=True, padx=10, pady=10) + + monitor_grid_frame.columnconfigure(0, weight=1) + monitor_grid_frame.columnconfigure(1, weight=1) + monitor_grid_frame.rowconfigure(0, weight=1) + monitor_grid_frame.rowconfigure(1, weight=1) + + global cpu_monitor, memory_monitor, disk_monitor, net_monitor + + cpu_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") + cpu_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + cpu_monitor = SystemMonitor(cpu_frame, 'cpu') + + memory_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") + memory_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) + memory_monitor = SystemMonitor(memory_frame, 'memory') + + disk_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") + disk_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) + disk_monitor = SystemMonitor(disk_frame, 'disk') + + net_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") + net_frame.grid(row=1, column=1, sticky="nsew", padx=5, pady=5) + net_monitor = SystemMonitor(net_frame, 'net') + +def setup_editor_tab(tab_frame): + tk.Label(tab_frame, text="Editor de Texto Simple", font=("Arial", 14, "bold")).pack(pady=10) + + editor_frame = tk.Frame(tab_frame) + editor_frame.pack(fill="both", expand=True, padx=10, pady=10) + + editor_scrollbar = ttk.Scrollbar(editor_frame) + editor_scrollbar.pack(side="right", fill="y") + + text_area = tk.Text( + editor_frame, + wrap="word", + undo=True, + font=("Consolas", 11), + yscrollcommand=editor_scrollbar.set + ) + text_area.pack(fill="both", expand=True) + editor_scrollbar.config(command=text_area.yview) + + ttk.Button( + tab_frame, + text="Guardar Documento", + command=lambda: save_text_file(text_area) + ).pack(pady=5) + +def setup_network_thread_tab(tab_frame): + """Configura la pestaña 'Hilo' con el contador de tráfico de red, centrado y ampliado.""" + global network_thread + + center_frame = tk.Frame(tab_frame) + center_frame.pack(expand=True) + + tk.Label( + center_frame, + text="Monitorización Total de Tráfico de Red (KB)", + font=("Arial", 18, "bold") + ).pack(pady=20) + + label_in = tk.Label( + center_frame, + text="KB Recibidos (Input): -- KB", + font=("Consolas", 20, "bold"), + fg="blue" + ) + label_in.pack(pady=15, padx=50) + + label_out = tk.Label( + center_frame, + text="KB Enviados (Output): -- KB", + font=("Consolas", 20, "bold"), + fg="red" + ) + label_out.pack(pady=15, padx=50) + + tk.Label( + center_frame, + text="El conteo se reinicia al navegar fuera de la pestaña T1.", + fg="gray", + font=("Arial", 10) + ).pack(pady=30) + + if not network_thread: + network_thread = NetworkTrafficThread(root, label_in, label_out) + + +def stop_network_thread(): + global network_thread + if 'network_thread' in globals() and network_thread: + network_thread.stop() + network_thread = None + + +def setup_t1_processes(tab_frame, root_instance): + """Configura el Notebook interno para la pestaña T1. Procesos.""" + for widget in tab_frame.winfo_children(): + widget.destroy() + + internal_notebook = ttk.Notebook(tab_frame) + internal_notebook.pack(fill="both", expand=True, padx=5, pady=5) + + estado_tab = ttk.Frame(internal_notebook) + internal_notebook.add(estado_tab, text="Estado") + setup_monitors_tab(estado_tab) + + editor_tab = ttk.Frame(internal_notebook) + internal_notebook.add(editor_tab, text="Editor de texto") + setup_editor_tab(editor_tab) + + hilo_tab = ttk.Frame(internal_notebook) + internal_notebook.add(hilo_tab, text="Hilo") + + def handle_internal_tab_change(event): + selected_index = internal_notebook.index(internal_notebook.select()) + current_tab_title = internal_notebook.tab(selected_index, "text") + + if internal_notebook.tab(0, "text") != "Estado": + stop_monitors() + + if current_tab_title == "Hilo": + if not network_thread: + setup_network_thread_tab(hilo_tab) + else: + stop_network_thread() + + internal_notebook.bind("<>", handle_internal_tab_change) + + +def stop_monitors(): + """Detiene todos los monitores y el hilo de red de T1.""" + global cpu_monitor, memory_monitor, disk_monitor, net_monitor + + if 'cpu_monitor' in globals() and cpu_monitor: + cpu_monitor.stop() + cpu_monitor = None + if 'memory_monitor' in globals() and memory_monitor: + memory_monitor.stop() + memory_monitor = None + if 'disk_monitor' in globals() and disk_monitor: + disk_monitor.stop() + disk_monitor = None + if 'net_monitor' in globals() and net_monitor: + net_monitor.stop() + net_monitor = None + + stop_network_thread() + +# --- FUNCIONES PARA LA PESTAÑA T2: RELOJ Y ALARMA --- + +def stop_clock_thread(): + """Detiene el hilo del reloj digital y el hilo de la alarma.""" + global clock_thread + if 'clock_thread' in globals() and clock_thread and hasattr(clock_thread, 'stop_event'): + clock_thread.stop_event.set() + clock_thread = None + + stop_alarm_countdown() + +def stop_alarm_countdown(): + """Detiene la cuenta atrás de la alarma si está activa.""" + global countdown_after_id, alarm_stop_event + + alarm_stop_event.set() + + if countdown_after_id: + root.after_cancel(countdown_after_id) + countdown_after_id = None + +def update_digital_clock(label_clock, stop_event): + """Función de bucle para actualizar la hora digital en un hilo.""" + while not stop_event.is_set(): + now = datetime.datetime.now() + time_str = now.strftime("%H:%M:%S") + date_str = now.strftime("%Y-%m-%d") + + display_text = f"{date_str}\n{time_str}" + + root.after(0, label_clock.config, {"text": display_text}) + time.sleep(1) + +def play_alarm_sound(): + """Produce una notificación sonora.""" + global MUSIC_AVAILABLE + if MUSIC_AVAILABLE: + # Detenemos la música de fondo para que la alarma se escuche clara + pygame.mixer.music.stop() + + try: + # Cargamos y reproducimos el archivo de alarma una sola vez + alarm_sound = pygame.mixer.Sound(ALARM_FILE) + alarm_sound.play() + except pygame.error as e: + messagebox.showerror("Error de Audio", f"No se pudo reproducir {ALARM_FILE}. Error: {e}") + + if WINSOUND_AVAILABLE: + # Fallback de Windows si Pygame no funciona bien + winsound.Beep(1000, 1000) + winsound.MessageBeep(winsound.MB_ICONEXCLAMATION) + else: + print("\n*** ALARMA DISPARADA (Sin sonido de sistema) ***\n") + +def alarm_loop(wait_seconds, alarm_time_str, label_status): + """Hilo de la alarma que espera y se dispara.""" + global alarm_stop_event + + time.sleep(wait_seconds) + + if alarm_stop_event.is_set(): + return + + root.after(0, play_alarm_sound) + root.after(0, lambda: messagebox.showinfo("ALARMA", f"¡ALARMA TERMINADA! Son las {alarm_time_str}")) + root.after(0, lambda: label_status.config(text="ALARMA TERMINADA", fg="red")) + root.after(0, stop_alarm_countdown) + +def start_alarm_thread(alarm_time_str, label_status, label_countdown): + """Calcula el tiempo de espera, inicia el hilo de alarma y la cuenta atrás.""" + global alarm_stop_event + + try: + alarm_hour, alarm_minute = map(int, alarm_time_str.split(':')) + + if not (0 <= alarm_hour <= 23 and 0 <= alarm_minute <= 59): + raise ValueError("Hora fuera de rango.") + + except ValueError: + messagebox.showerror("Error de Formato", "Por favor, introduce la hora en formato HH:MM (ej: 07:30)") + return + + now = datetime.datetime.now() + + alarm_dt = now.replace(hour=alarm_hour, minute=alarm_minute, second=0, microsecond=0) + + if alarm_dt <= now: + alarm_dt += datetime.timedelta(days=1) + + time_difference = alarm_dt - now + wait_seconds = time_difference.total_seconds() + + stop_alarm_countdown() + alarm_stop_event.clear() + + label_status.config(text=f"ALARMA ACTIVA: {alarm_time_str} (Hacia adelante)", fg="orange") + update_countdown_label(alarm_dt, label_countdown, label_status) + + threading.Thread(target=alarm_loop, args=(wait_seconds, alarm_time_str, label_status), daemon=True).start() + +def update_countdown_label(alarm_dt, label_countdown, label_status): + """Actualiza la etiqueta de cuenta atrás cada segundo.""" + global countdown_after_id, alarm_stop_event + + now = datetime.datetime.now() + + if alarm_stop_event.is_set() or alarm_dt <= now: + label_countdown.config(text="00:00:00") + label_status.config(text="ALARMA INACTIVA", fg="gray") + return + + time_remaining = alarm_dt - now + + total_seconds = int(time_remaining.total_seconds()) + + hours, remainder = divmod(total_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + countdown_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}" + + label_countdown.config(text=countdown_str) + + countdown_after_id = root.after(1000, lambda: update_countdown_label(alarm_dt, label_countdown, label_status)) + + +def set_alarm(entry_hour, label_status, label_countdown): + """Recoge la hora de entrada y lanza el hilo de alarma.""" + alarm_time = entry_hour.get() + if alarm_time: + start_alarm_thread(alarm_time, label_status, label_countdown) + entry_hour.delete(0, tk.END) + +def setup_clock_tab(tab_frame): + """Configura la interfaz de la subpestaña Reloj/Alarma.""" + for widget in tab_frame.winfo_children(): + widget.destroy() + + center_frame = tk.Frame(tab_frame) + center_frame.pack(expand=True, pady=20) + + tk.Label(center_frame, text="Reloj Digital y Alarma", font=("Arial", 16, "bold")).pack(pady=10) + + label_clock = tk.Label( + center_frame, + text="00:00:00", + font=("Consolas", 60, "bold"), + bg="black", + fg="#00FF00", + relief="sunken", + padx=20, pady=10 + ) + label_clock.pack(pady=20) + + global clock_thread + stop_clock_thread() + + stop_event = threading.Event() + clock_thread = threading.Thread( + target=update_digital_clock, + args=(label_clock, stop_event), + daemon=True + ) + clock_thread.stop_event = stop_event + clock_thread.start() + + alarm_frame = ttk.LabelFrame(center_frame, text="Configurar Alarma", padding=10) + alarm_frame.pack(pady=30, padx=20, fill="x") + + tk.Label(alarm_frame, text="Hora (HH:MM):", font=("Arial", 12)).pack(side="left", padx=5) + + entry_hour = ttk.Entry(alarm_frame, width=8, font=("Arial", 12)) + entry_hour.pack(side="left", padx=5) + + label_alarm_status = tk.Label(alarm_frame, text="ALARMA INACTIVA", fg="gray", font=("Arial", 12, "italic")) + label_alarm_status.pack(side="right", padx=15) + + label_countdown = tk.Label(alarm_frame, text="00:00:00", fg="red", font=("Consolas", 14, "bold")) + label_countdown.pack(side="right", padx=10) + + ttk.Button( + alarm_frame, + text="Establecer Alarma", + command=lambda: set_alarm(entry_hour, label_alarm_status, label_countdown) + ).pack(side="right", padx=10) + + ttk.Button( + alarm_frame, + text="Cancelar", + command=lambda: stop_alarm_countdown() or label_alarm_status.config(text="ALARMA CANCELADA", fg="gray") + ).pack(side="right", padx=10) + +# --- FUNCIONES PARA LA PESTAÑA T2: COCHES --- + +def stop_race(): + """Detiene la carrera actual, si está en curso.""" + global race_stop_event, race_threads, winner + + if not race_stop_event.is_set(): + race_stop_event.set() + + for thread in race_threads: + if thread.is_alive(): + pass + + race_threads = [] + winner = None + +def car_movement(canvas, car_id, car_color, label_result, start_x, goal_x): + """Hilo que mueve un coche de color.""" + global winner, race_lock + + current_x = start_x + delay = random.randint(10, 50) / 1000.0 + step = 5 + + while current_x < goal_x and not race_stop_event.is_set(): + current_x += step + + canvas.after(0, canvas.coords, car_id, current_x, canvas.coords(car_id)[1], current_x + 50, canvas.coords(car_id)[3]) + + time.sleep(delay) + + if not race_stop_event.is_set(): + with race_lock: + if winner is None: + winner = car_color + + race_stop_event.set() + + root.after(0, label_result.config, {"text": f"🎉 ¡Ha ganado el coche {car_color.upper()}! 🎉", "fg": car_color, "font": ("Arial", 18, "bold")}) + + status_manager.set_status("status_3", f"Ganador: {car_color}", car_color) + + else: + pass + +def start_race(canvas, cars, label_result): + """Inicia la carrera, lanzando un hilo para cada coche.""" + + stop_race() + + global race_threads, winner, race_stop_event + + winner = None + race_stop_event.clear() + + label_result.config(text="¡CARRERA EN CURSO!", fg="black", font=("Arial", 14, "italic")) + + START_X = 10 + GOAL_X = canvas.winfo_width() - 60 + + for car_color, car_id in cars: + + canvas.coords(car_id, START_X, canvas.coords(car_id)[1], START_X + 50, canvas.coords(car_id)[3]) + + thread = threading.Thread( + target=car_movement, + args=(canvas, car_id, car_color, label_result, START_X, GOAL_X), + daemon=True + ) + race_threads.append(thread) + thread.start() + + status_manager.set_status("status_3", "Carrera iniciada...", "yellow") + + +def setup_race_game(tab_frame): + """Configura la interfaz del juego de la carrera de coches.""" + for widget in tab_frame.winfo_children(): + widget.destroy() + + control_frame = tk.Frame(tab_frame) + control_frame.pack(pady=10, fill="x") + + tk.Label(control_frame, text="Simulación de Carrera Multihilo (Coches)", font=("Arial", 16, "bold")).pack(side="left", padx=15) + + label_result = tk.Label(control_frame, text="Presiona 'START' para comenzar", fg="black", font=("Arial", 14)) + label_result.pack(side="right", padx=15) + + canvas_frame = tk.Frame(tab_frame, bd=2, relief="sunken") + canvas_frame.pack(fill="both", expand=True, padx=10, pady=5) + + canvas = tk.Canvas(canvas_frame, bg="lightgray") + canvas.pack(fill="both", expand=True) + + CARS_CONFIG = [ + ("red", 50), + ("blue", 150), + ("green", 250) + ] + + cars_data = [] + + def draw_cars_and_goal(): + canvas_width = canvas.winfo_width() + if canvas_width < 100: + root.after(100, draw_cars_and_goal) + return + + GOAL_X = canvas_width - 50 + canvas.create_line(GOAL_X, 0, GOAL_X, canvas.winfo_height(), width=3, fill="black", dash=(5, 5)) + canvas.create_text(GOAL_X - 20, 15, text="META", fill="black") + + for color, y_pos in CARS_CONFIG: + car_id = canvas.create_rectangle(10, y_pos, 60, y_pos + 50, fill=color, outline="black") + cars_data.append((color, car_id)) + + canvas.bind("", lambda event: draw_cars_and_goal() if not cars_data else None) + + button_frame = tk.Frame(tab_frame) + button_frame.pack(pady=10) + + ttk.Button( + button_frame, + text="START RACE (Hilos)", + command=lambda: start_race(canvas, cars_data, label_result) + ).pack(side="left", padx=10) + + ttk.Button( + button_frame, + text="STOP/RESET", + command=lambda: stop_race() or label_result.config(text="Carrera detenida y reiniciada.", fg="gray") + ).pack(side="left", padx=10) + + +def setup_t2_threads(tab_frame): + """Configura el Notebook interno para la pestaña T2. Threads.""" + for widget in tab_frame.winfo_children(): + widget.destroy() + + internal_notebook = ttk.Notebook(tab_frame) + internal_notebook.pack(fill="both", expand=True, padx=5, pady=5) + + clock_tab = ttk.Frame(internal_notebook) + internal_notebook.add(clock_tab, text="Reloj / Alarma") + + race_tab = ttk.Frame(internal_notebook) + internal_notebook.add(race_tab, text="Coches") + + # --- PESTAÑA DE SCRAPING --- + scraping_tab = ttk.Frame(internal_notebook) + internal_notebook.add(scraping_tab, text="Scraping") + # --------------------------------- + + def handle_internal_tab_change_t2(event): + selected_index = internal_notebook.index(internal_notebook.select()) + current_tab_title = internal_notebook.tab(selected_index, "text") + + # Detener hilos al cambiar de pestaña + if current_tab_title != "Reloj / Alarma": + stop_clock_thread() + else: + setup_clock_tab(clock_tab) + + if current_tab_title != "Coches": + stop_race() + else: + setup_race_game(race_tab) + + if current_tab_title == "Scraping": + setup_scraping_tab(scraping_tab) + + internal_notebook.bind("<>", handle_internal_tab_change_t2) + + # Configuración inicial de todas las pestañas + setup_clock_tab(clock_tab) + setup_race_game(race_tab) + setup_scraping_tab(scraping_tab) + + +# --- FUNCIÓN DE NAVEGACIÓN PRINCIPAL --- + +def navigate_to_tab(event): + """Maneja el cambio de pestaña principal (T1, T2, T3...).""" + selected_index = notebook.index(notebook.select()) + tab_name = notebook.tab(selected_index, "text") + + # 1. Limpiar el frame izquierdo (Botones) + for widget in frame_izquierdo.winfo_children(): + widget.destroy() + + # 2. Detener todos los hilos activos de otras pestañas + stop_monitors() + stop_clock_thread() + stop_race() + stop_music_clean() # Detener la música al cambiar de pestaña + + # 3. Configurar la pestaña principal y los botones laterales + if selected_index == 0: # T1. Procesos + setup_t1_processes(tabs[0], root) + setup_buttons_t1(frame_izquierdo) + elif selected_index == 1: # T2. Threads + setup_t2_threads(tabs[1]) + setup_buttons_t2(frame_izquierdo) + initialize_music() # Cargar la música solo cuando entramos en T2 + elif selected_index == 2: # T3. Sockets (Pendiente de implementación) + setup_buttons_default(frame_izquierdo, tab_name) + else: + setup_buttons_default(frame_izquierdo, tab_name) + + status_manager.set_status("status_1", f"Navegando a: {tab_name}", "yellow") + root.after(2000, lambda: status_manager.set_status("status_1", "Navegación completa", "green")) + + +# --- FUNCIONES DE SETUP PARA BOTONES CONTEXTUALES --- + +def create_section(parent, title): + """Crea un label de título de sección estilizado.""" + tk.Label(parent, text=title, bg="#AAAAAA", fg="black", font=("Arial", 10, "bold"), anchor="w").pack(side="top", fill="x", pady=(10, 2), padx=5) + + +def setup_buttons_t1(frame): + """Configura los botones para la pestaña T1. Procesos.""" + URL_GOOGLE = "https://www.google.com" + URL_YOUTUBE = "https://www.youtube.com" + URL_FILMIN = "https://www.filmin.es/" + + create_section(frame, "Aplicaciones") + + ttk.Button(frame, text="Google", command=lambda: open_external_url(URL_GOOGLE)).pack(pady=3, padx=10, fill="x") + ttk.Button(frame, text="YouTube", command=lambda: open_external_url(URL_YOUTUBE)).pack(pady=3, padx=10, fill="x") + ttk.Button(frame, text="Filmin", command=lambda: open_external_url(URL_FILMIN)).pack(pady=3, padx=10, fill="x") + + create_section(frame, "Procesos batch") + ttk.Button( + frame, + text="Copias de seguridad", + command=run_bash_backup + ).pack(pady=3, padx=10, fill="x") + +def setup_buttons_t2(frame): + """Configura los botones para la pestaña T2. Threads (Incluyendo el de Música).""" + create_section(frame, "Hilos de Control") + + # Crear un botón con estilo y pasarlo a la función de toggle + global music_state + + # Definir el estilo inicial + initial_text = "MÚSICA (Play)" if not music_state else "STOP MÚSICA (En Bucle)" + initial_style = "Green.TButton" if not music_state else "Red.TButton" + + music_button = ttk.Button( + frame, + text=initial_text, + style=initial_style + ) + # Pasar el botón a la función toggle_music para que pueda cambiar su propio texto y estilo + music_button.config(command=lambda: toggle_music(music_button)) + music_button.pack(pady=10, padx=10, fill="x") + + create_section(frame, "Configuración General") + tk.Label(frame, text="Controles de T2...", fg="gray", font=("Arial", 10)).pack(pady=10) + + +def setup_buttons_default(frame, tab_title): + """Configura botones genéricos para pestañas no implementadas.""" + create_section(frame, f"Acciones para {tab_title}") + tk.Label(frame, text="Funcionalidad pendiente...", fg="gray", font=("Arial", 10)).pack(pady=10) + ttk.Button(frame, text="Ejecutar Acción Genérica").pack(pady=3, padx=10, fill="x") + + +# --- CONFIGURACIÓN PRINCIPAL --- +root = tk.Tk() +root.title("Ventana Responsive - Proyecto Nico") +root.geometry("1200x800") + +root.columnconfigure(0, weight=0) +root.columnconfigure(1, weight=1) +root.rowconfigure(0, weight=1) +root.rowconfigure(1, weight=0) + +# Menú +menu_bar = Menu(root) +file_menu = Menu(menu_bar, tearoff=0) +file_menu.add_command(label="Nuevo") +file_menu.add_command(label="Abrir") +file_menu.add_separator() +file_menu.add_command(label="Salir", command=root.quit) +edit_menu = Menu(menu_bar, tearoff=0) +edit_menu.add_command(label="Copiar") +edit_menu.add_command(label="Pegar") +help_menu = Menu(menu_bar, tearoff=0) +help_menu.add_command(label="Acerca de") +menu_config = Menu(menu_bar, tearoff=0) +menu_config.add_command(label="Preferencias") +menu_bar.add_cascade(label="Archivo", menu=file_menu) +menu_bar.add_cascade(label="Editar", menu=edit_menu) +menu_bar.add_cascade(label="Configuración", menu=menu_config) +menu_bar.add_cascade(label="Ayuda", menu=help_menu) +root.config(menu=menu_bar) + +# Estilos personalizados +style = ttk.Style() +style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold")) +style.configure("Green.TButton", background="#4CAF50", foreground="black") +style.map("Green.TButton", + background=[('active', '#66BB6A')], + foreground=[('active', 'black')] +) +style.configure("Red.TButton", background="#F44336", foreground="black") +style.map("Red.TButton", + background=[('active', '#E57373')], + foreground=[('active', 'black')] +) + +# Estructura de Frames +frame_izquierdo = tk.Frame(root, bg="#DDDDDD", width=250) +frame_central = tk.Frame(root, bg="white") + +frame_izquierdo.grid(row=0, column=0, sticky="ns") +frame_central.grid(row=0, column=1, sticky="nsew") + +frame_izquierdo.grid_propagate(False) + +frame_central.rowconfigure(0, weight=1) +frame_central.rowconfigure(1, weight=0) +frame_central.columnconfigure(0, weight=1) + +frame_superior = tk.Frame(frame_central, bg="#F0F0F0") +frame_inferior = tk.Frame(frame_central, bg="#D0FFD0", height=100) + +frame_superior.grid(row=0, column=0, sticky="nsew") +frame_inferior.grid(row=1, column=0, sticky="ew") + +frame_inferior.grid_propagate(False) +tk.Label(frame_inferior, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", bg="#D0FFD0", fg="#333333", justify="left", padx=10, pady=5).pack(fill="both", expand=True) + +# Barra de estado +barra_estado = tk.Frame(root, bg="lightgray") +barra_estado.grid(row=1, column=0, columnspan=2, sticky="ew") +barra_estado.grid_columnconfigure(0, weight=1) + +status_manager = StatusBarManager(barra_estado, root) + +label_fecha_hora = tk.Label(barra_estado, text="Hilo fecha-hora", font=("Helvetica", 10, "bold"), bd=1, fg="blue", relief="raised", anchor="center", width=25, padx=5) +label_fecha_hora.pack(side="right", fill="x", padx=5) + +# INICIALIZACIÓN DEL HILO DE LA BARRA DE ESTADO +stop_event_status_bar = threading.Event() +status_bar_time_thread = threading.Thread(target=update_time, args=(label_fecha_hora, stop_event_status_bar), daemon=True) +status_bar_time_thread.stop_event = stop_event_status_bar +status_bar_time_thread.start() + +# Notebook principal +notebook = ttk.Notebook(frame_superior, style="CustomNotebook.TNotebook") +notebook.pack(fill="both", expand=True) + +TAB_TITLES = ["T1. Procesos", "T2. Threads", "T3. Sockets", "T4. Servicios", "T5. Seguridad"] +tabs = [] + +for i, title in enumerate(TAB_TITLES): + tab = ttk.Frame(notebook) + tabs.append(tab) + notebook.add(tab, text=title, padding=4) + + if i == 0: + pass + elif i == 1: + pass + else: + ttk.Label(tab, text=f"Contenido de {title}", font=("Arial", 14)).pack(pady=20) + +notebook.bind("<>", navigate_to_tab) + +# Configuración inicial (inicia en T1) +root.after(100, lambda: setup_t1_processes(tabs[0], root)) +root.after(100, lambda: setup_buttons_t1(frame_izquierdo)) + + +# FUNCIÓN DE CIERRE DE VENTANA +def on_closing(): + stop_monitors() + stop_clock_thread() + stop_race() + stop_music_clean() + stop_status_bar_time_thread() + root.destroy() + +root.protocol("WM_DELETE_WINDOW", on_closing) +root.mainloop() \ No newline at end of file diff --git a/ProyectoAdrianHustea/alarm.mp3 b/ProyectoAdrianHustea/alarm.mp3 new file mode 100644 index 0000000..36abcb3 Binary files /dev/null and b/ProyectoAdrianHustea/alarm.mp3 differ diff --git a/ProyectoAdrianHustea/backup_script.sh b/ProyectoAdrianHustea/backup_script.sh new file mode 100644 index 0000000..06d7405 Binary files /dev/null and b/ProyectoAdrianHustea/backup_script.sh differ diff --git a/ProyectoAdrianHustea/music.mp3 b/ProyectoAdrianHustea/music.mp3 new file mode 100644 index 0000000..55958b7 Binary files /dev/null and b/ProyectoAdrianHustea/music.mp3 differ