Subir archivos a "/"

This commit is contained in:
mireya 2025-12-07 19:57:53 +00:00
parent d0d36a3957
commit 54a1b8de61
5 changed files with 536 additions and 0 deletions

173
alarm_logic.py Normal file
View File

@ -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()

72
audio_player_logic.py Normal file
View File

@ -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

56
backup_logic.py Normal file
View File

@ -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

177
camel_game_logic.py Normal file
View File

@ -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.")

58
external_launcher.py Normal file
View File

@ -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