diff --git a/alarm_logic.py b/alarm_logic.py new file mode 100644 index 0000000..156ba10 --- /dev/null +++ b/alarm_logic.py @@ -0,0 +1,173 @@ +import datetime +import shelve +import os + +# Nombre del archivo de base de datos para persistencia +DB_FILE = 'alarm_data.db' + +def get_db_path(): + """Obtiene la ruta completa para el archivo shelve.""" + return os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_FILE) + +def load_alarms(): + """Carga todas las alarmas persistentes del archivo shelve.""" + alarms = [] + try: + with shelve.open(get_db_path()) as db: + if 'alarms' in db: + alarms = list(db['alarms']) + except Exception as e: + print(f"Error al cargar alarmas: {e}") + return alarms + +def save_alarms(alarms): + """Guarda la lista completa de alarmas en el archivo shelve.""" + try: + with shelve.open(get_db_path(), writeback=True) as db: + db['alarms'] = alarms + except Exception as e: + print(f"Error al guardar alarmas: {e}") + +def add_alarm(hour, minute, message): + """Añade una nueva alarma y la guarda.""" + alarms = load_alarms() + new_id = max([a['id'] for a in alarms] + [0]) + 1 + + new_alarm = { + 'id': new_id, + 'hour': int(hour), + 'minute': int(minute), + 'message': message, + 'active': True + } + alarms.append(new_alarm) + save_alarms(alarms) + return alarms + +def delete_alarm(alarm_id): + """Elimina una alarma por su ID.""" + alarms = load_alarms() + alarms = [alarm for alarm in alarms if alarm['id'] != alarm_id] + save_alarms(alarms) + return alarms + +def toggle_alarm(alarm_id, active_state): + """Cambia el estado activo de una alarma.""" + alarms = load_alarms() + for alarm in alarms: + if alarm['id'] == alarm_id: + alarm['active'] = active_state + break + save_alarms(alarms) + return alarms + +def postpone_alarm(alarm_id): + """ + Posiciona la alarma un minuto más tarde y la marca como activa. + IMPORTANTE: Esto sobrescribe la hora original de la alarma. + """ + alarms = load_alarms() + for alarm in alarms: + if alarm['id'] == alarm_id: + # Creamos un objeto datetime temporal para calcular la nueva hora + current_time = datetime.datetime(1, 1, 1, alarm['hour'], alarm['minute']) + new_time = current_time + datetime.timedelta(minutes=1) + + alarm['hour'] = new_time.hour + alarm['minute'] = new_time.minute + alarm['active'] = True # Vuelve a estar activa para sonar en 1 minuto + break + save_alarms(alarms) + return alarms + +def check_alarms(root, alarm_list_ref, notify_callback): + """ + Función principal de verificación de alarmas. + Utiliza una función de callback para mostrar el POP-UP. + """ + now = datetime.datetime.now() + current_hour = now.hour + current_minute = now.minute + + alarms = load_alarms() + + for alarm in alarms: + if alarm['active'] and alarm['hour'] == current_hour and alarm['minute'] == current_minute: + + # 1. Llamar a la función de notificación del POP-UP en main_app + notify_callback(alarm) + + # 2. Desactivar temporalmente la alarma en la lógica interna (en caso de que el usuario no la posponga o detenga) + # Esto previene que suene en cada segundo dentro del mismo minuto. + alarm['active'] = False + + # 3. Guardar el cambio de estado + save_alarms(alarms) + + # 4. Actualizar la lista visible en la UI + alarm_list_ref.update_list(alarms) + break + + # Volver a programar la comprobación para el siguiente segundo + root.after(1000, check_alarms, root, alarm_list_ref, notify_callback) + + + + + + # alarm_logic.py (COMIENZO DEL ARCHIVO) +import datetime +import shelve +import os +import tkinter as tk # NECESARIO para Toplevel y otros widgets +from tkinter import ttk # NECESARIO si usas widgets ttk +# ... (rest of the initial code) ... + +# ... (all functions like load_alarms, postpone_alarm, check_alarms, etc.) ... + +# --- NUEVA CLASE MOVIDA DESDE main_app.py --- +class CustomAlarmDialog(tk.Toplevel): + """Ventana de diálogo personalizada para la alarma con botones renombrados.""" + def __init__(self, master, alarm_data): + super().__init__(master) + self.transient(master) + self.title("🔔 ¡ALARMA ACTIVA!") + self.result = None + + self.grab_set() + self.focus_set() + + self.geometry("300x150") + self.resizable(False, False) + + message_text = f"ALARMA SONANDO!\n\nMensaje: {alarm_data['message']}" + + tk.Label(self, text="⚠️", font=("Arial", 20)).pack(pady=5) + tk.Label(self, text=message_text, font=("Arial", 10, "bold")).pack(pady=5) + + button_frame = tk.Frame(self) + button_frame.pack(pady=10) + + # Botón POSPONER + tk.Button(button_frame, text="POSPONER (1 min)", command=lambda: self.on_action('posponer'), + bg='blue', fg='white').pack(side=tk.LEFT, padx=10) + + # Botón DETENER + tk.Button(button_frame, text="DETENER", command=lambda: self.on_action('detener'), + bg='red', fg='white').pack(side=tk.LEFT, padx=10) + + self.protocol("WM_DELETE_WINDOW", lambda: self.on_action('detener')) + + self.master.update_idletasks() + width = self.winfo_width() + height = self.winfo_height() + x = (self.master.winfo_width() // 2) - (width // 2) + y = (self.master.winfo_height() // 2) - (height // 2) + self.geometry('+%d+%d' % (self.master.winfo_x() + x, self.master.winfo_y() + y)) + + self.master.wait_window(self) + + def on_action(self, action): + self.result = action + self.grab_release() + self.destroy() \ No newline at end of file diff --git a/audio_player_logic.py b/audio_player_logic.py new file mode 100644 index 0000000..094b074 --- /dev/null +++ b/audio_player_logic.py @@ -0,0 +1,72 @@ +import pygame as pg +from tkinter import messagebox +import os + +class MusicPlayer: + def __init__(self, root_app): + self.root_app = root_app + self.current_file = None + self.is_playing = False + + try: + # Inicializar el mezclador de Pygame. + # Frecuencia, tamaño de bits, canales, y tamaño del buffer (menor buffer = menor latencia) + pg.mixer.init(frequency=44100, size=-16, channels=2, buffer=512) + except pg.error as e: + messagebox.showerror("Error de Audio", f"No se pudo inicializar Pygame Mixer. Asegúrate de tener hardware de audio: {e}") + + def load_and_play(self, filepath): + """Carga un archivo de música y comienza la reproducción.""" + if not os.path.exists(filepath): + messagebox.showerror("Error de Archivo", "El archivo de música no se encuentra.") + return + + try: + # Detener cualquier reproducción actual + pg.mixer.music.stop() + + # Cargar el nuevo archivo + pg.mixer.music.load(filepath) + self.current_file = filepath + + # Reproducir la música en bucle (-1) + pg.mixer.music.play(-1) + self.is_playing = True + + self.root_app.after(0, self.root_app.update_audio_status, f"Reproduciendo: {os.path.basename(filepath)}") + return True + + except pg.error as e: + messagebox.showerror("Error de Reproducción", f"No se pudo reproducir el archivo: {e}") + self.is_playing = False + return False + + def pause_music(self): + """Pausa la música si está reproduciéndose.""" + if self.is_playing and pg.mixer.music.get_busy(): + pg.mixer.music.pause() + self.is_playing = False + self.root_app.after(0, self.root_app.update_audio_status, "Música: Pausada") + return True + return False + + def unpause_music(self): + """Reanuda la música si está en pausa.""" + if not self.is_playing and self.current_file: + pg.mixer.music.unpause() + self.is_playing = True + self.root_app.after(0, self.root_app.update_audio_status, f"Reproduciendo: {os.path.basename(self.current_file)}") + return True + return False + + def stop_music(self): + """Detiene completamente la música y libera el recurso.""" + if pg.mixer.music.get_busy(): + pg.mixer.music.stop() + self.is_playing = False + self.current_file = None + self.root_app.after(0, self.root_app.update_audio_status, "Música: Detenida") + return True + return False + +# Fin de audio_player_logic.py \ No newline at end of file diff --git a/backup_logic.py b/backup_logic.py new file mode 100644 index 0000000..b068220 --- /dev/null +++ b/backup_logic.py @@ -0,0 +1,56 @@ +import json +import os +from tkinter import messagebox + +# Nombre del archivo donde se guardará la copia de seguridad +BACKUP_FILE = "app_backup.json" + +def create_backup(alarms): + """ + Guarda el estado actual de las alarmas en un archivo JSON. + :param alarms: La lista de alarmas a guardar. + """ + try: + # Nota: Asumo que 'alarms' es la lista de alarmas obtenida, por ejemplo, de al.load_alarms() + # Si las alarmas son estructuras complejas (ej. clases), necesitarás serializarlas + # correctamente, pero para una lista de diccionarios, JSON es suficiente. + + with open(BACKUP_FILE, 'w', encoding='utf-8') as f: + json.dump({'alarms': alarms}, f, indent=4) + + messagebox.showinfo("Copia de Seguridad", f"Copia de seguridad creada con éxito en:\n{os.path.abspath(BACKUP_FILE)}") + return True + + except Exception as e: + messagebox.showerror("Error de Copia de Seguridad", f"Error al crear la copia de seguridad: {e}") + return False + +def restore_backup(): + """ + Carga el estado de las alarmas desde el archivo de copia de seguridad. + :return: La lista de alarmas cargada o None si falla. + """ + if not os.path.exists(BACKUP_FILE): + messagebox.showwarning("Restauración", "No se encontró ningún archivo de copia de seguridad.") + return None + + try: + with open(BACKUP_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + alarms = data.get('alarms', []) + + # Aquí necesitarías una función en 'alarm_logic.py' para sobrescribir + # el estado actual con el estado restaurado. Si 'alarm_logic' usa un + # archivo persistente, podrías reemplazar ese archivo con el contenido + # de 'alarms', o llamar a una función para guardar el estado. + + # Nota: Dado que 'alarm_logic.py' no está completo aquí, + # se devuelve la lista de alarmas y se debe manejar en el main. + + return alarms + + except Exception as e: + messagebox.showerror("Error de Restauración", f"Error al cargar la copia de seguridad: {e}") + return None + +# Fin de backup_logic.py \ No newline at end of file diff --git a/camel_game_logic.py b/camel_game_logic.py new file mode 100644 index 0000000..b636b42 --- /dev/null +++ b/camel_game_logic.py @@ -0,0 +1,177 @@ +import threading +import time +import random +import tkinter as tk +from tkinter import messagebox + +# Definición de los recursos (Zonas de la Pista) +NUM_LOCKS = 5 +# Crea una lista de RLock que representan las zonas de la pista (recursos) +TRACK_LOCKS = [threading.RLock() for _ in range(NUM_LOCKS)] + +class Camel(threading.Thread): + def __init__(self, name, track_length, root_app, status_label, emoji): + super().__init__() + self.name = name + self.track_length = track_length + self.distance = 0 + self.is_running = True + self.root_app = root_app + self.status_label = status_label + self.emoji = emoji + # No necesitamos self.winner, usamos root_app.winner_name + + def run(self): + # La simulación de la carrera + while self.distance < self.track_length and self.is_running: + + # --- Lógica de Sincronización (Adquisición Ordenada de Locks) --- + + # El camello necesita dos zonas adyacentes para avanzar + lock_id_1 = self.distance % NUM_LOCKS + lock_id_2 = (self.distance + 1) % NUM_LOCKS + + # Regla para PREVENIR DEADLOCK: Adquirir el lock con el ID más bajo primero. + lock_a = TRACK_LOCKS[min(lock_id_1, lock_id_2)] + lock_b = TRACK_LOCKS[max(lock_id_1, lock_id_2)] + + # Intento de adquisición ordenada de los recursos + with lock_a: + self.update_ui(f"{self.name} asegurando zona {min(lock_id_1, lock_id_2)}...", message_only=True) + time.sleep(random.uniform(0.01, 0.05)) + + with lock_b: + + # Movimiento: incrementar la distancia (simula el avance) + move_distance = random.randint(1, 3) + self.distance += move_distance + + if self.distance > self.track_length: + self.distance = self.track_length + + # Comprobar si ha ganado DENTRO de la sección crítica + if self.distance >= self.track_length and self.root_app.winner_name is None: + + # El primero en llegar establece el ganador y detiene a todos + self.root_app.winner_name = self.name + self.is_running = False + self.root_app.after(0, lambda: self.root_app.show_winner(self.name)) + + # Detener el resto de hilos + for thread in self.root_app.camel_threads: + if thread.name != self.name: + thread.is_running = False + + # Actualizar la UI con la nueva posición después de liberarse de los locks + self.update_ui(f"{self.name} avanzando.") + time.sleep(random.uniform(0.1, 0.5)) + + # --- Lógica al finalizar el hilo --- + if self.distance >= self.track_length: + self.update_ui(f"¡{self.name} ha cruzado la meta!", final=True) + else: + self.update_ui(f"{self.name} detenido.", final=True) + + + def update_ui(self, message, final=False, message_only=False): + """Actualiza la interfaz de usuario de forma segura.""" + + # 1. Crear la representación visual de la pista + # 40 espacios representan la pista visual en la etiqueta + track_display_length = 40 + + # Calcular la posición del camello en la pista virtual (0 a track_display_length) + position_on_display = int((self.distance / self.track_length) * track_display_length) + + # Asegurarse de que no se salga del inicio + if position_on_display < 0: + position_on_display = 0 + + # Construir la cadena de la pista + + # Espacios de pista antes del camello + pre_camel_track = "░" * position_on_display + + # Espacios de pista después del camello + post_camel_length = track_display_length - len(pre_camel_track) - len(self.emoji) + if post_camel_length < 0: + post_camel_length = 0 + + post_camel_track = "░" * post_camel_length + + # Ensamblar la pista visual + track_str = pre_camel_track + self.emoji + post_camel_track + + # Si el camello ha ganado, el emoji siempre debe estar al final + if final and self.distance >= self.track_length: + track_str = "░" * (track_display_length - len(self.emoji)) + self.emoji + + # 2. Actualizar el mensaje de actividad general (Estado 1) + self.root_app.after(0, self.root_app.update_activity_status, message) + + if message_only: + return + + # 3. Mensaje a mostrar en la etiqueta de su camello + display_message = f"{self.name}\n[{track_str}]" + + if final and self.distance >= self.track_length: + display_message = f"🎉 ¡HA GANADO {self.name}! 🎉\n[{track_str}]" + + elif final: + display_message = f"🛑 {self.name} detenido antes de meta." + + + # 4. Actualizar la UI + self.root_app.after(0, self.status_label.config, {'text': display_message}) + + +def start_camel_game(root_app, track_length=50): + """Inicializa y comienza la carrera de camellos.""" + + camel_data = [ + {"name": "Sahara Runner", "emoji": "🐪"}, + {"name": "Dune Master", "emoji": "🐫"}, + {"name": "Oasis King", "emoji": "👑"}, + {"name": "Mirage Express", "emoji": "🏎️"} + ] + + root_app.camel_threads = [] + root_app.winner_name = None + root_app.clear_camel_game_area() + root_app.track_length = track_length + + # Etiqueta que define el inicio y la meta + tk.Label( + root_app.frame_game, + text=f"INICIO \t\t\t\t\t\t\t\t\t\t META", + font=("Consolas", 12, "bold"), + fg='#006400', + bg="#D4EDDA" + ).pack(fill='x', padx=10, pady=(5, 0)) + + + # Crear las etiquetas de estado para cada camello en la interfaz + for data in camel_data: + # Usamos tk.Label con anchor 'w' para que se alineen bien + status_label = tk.Label( + root_app.frame_game, + text=f"{data['name']} | 🏁 Iniciando...", + anchor='w', + justify=tk.LEFT, + font=("Consolas", 12), + bg="#F0F0F0", + padx=10, + pady=5, + relief=tk.RAISED + ) + status_label.pack(fill='x', padx=10, pady=5) + + camel = Camel(data['name'], track_length, root_app, status_label, data['emoji']) + root_app.camel_threads.append(camel) + + # Iniciar todos los hilos (camellos) + for camel in root_app.camel_threads: + camel.start() + + root_app.after(0, root_app.update_activity_status, "¡LA CARRERA HA COMENZADO! Hilos competidores activos.") \ No newline at end of file diff --git a/external_launcher.py b/external_launcher.py new file mode 100644 index 0000000..040546e --- /dev/null +++ b/external_launcher.py @@ -0,0 +1,58 @@ +import subprocess +import platform +from tkinter import messagebox +import os + +def launch_browser(url): + """ + Lanza la URL proporcionada usando el navegador predeterminado del sistema. + :param url: La URL a abrir. + """ + if not url.startswith(('http://', 'https://')): + url = 'http://' + url + + try: + # Usa webbrowser.open para abrir URLs de manera segura y multiplataforma + import webbrowser + webbrowser.open(url) + return True + except Exception as e: + messagebox.showerror("Error de Navegador", f"No se pudo abrir el navegador predeterminado: {e}") + return False + +def launch_custom_app(command_list, app_name="Aplicación"): + """ + Lanza una aplicación externa usando una lista de comandos y argumentos. + + Ejemplo de command_list: ['notepad.exe', 'C:\\ruta\\a\\archivo.txt'] + :param command_list: Una lista que contiene el ejecutable y sus argumentos. + :param app_name: Nombre de la aplicación para mensajes de error. + """ + try: + # Usamos subprocess.Popen para lanzar la aplicación sin bloquear el hilo principal (GUI) + # Esto permite que la aplicación externa se abra y la GUI siga respondiendo. + subprocess.Popen(command_list) + return True + + except FileNotFoundError: + messagebox.showerror(f"Error al lanzar {app_name}", + f"El ejecutable '{command_list[0]}' no fue encontrado en el sistema (PATH).") + return False + except Exception as e: + messagebox.showerror(f"Error al lanzar {app_name}", f"Ocurrió un error inesperado: {e}") + return False + +def get_platform_browser_command(url): + """Devuelve la lista de comandos para abrir el navegador según el SO.""" + + # Intenta usar Chrome si está disponible, si no, usa el predeterminado. + if platform.system() == "Windows": + return ["cmd", "/c", "start", "chrome", url] + elif platform.system() == "Linux": + # 'google-chrome' o 'xdg-open' (para predeterminado) + return ["google-chrome", url] + elif platform.system() == "Darwin": # macOS + return ["open", "-a", "Google Chrome", url] + else: + # Si no se reconoce el SO, recurrimos al método seguro de webbrowser + return None \ No newline at end of file