Subir archivos a "/"
This commit is contained in:
parent
d0d36a3957
commit
54a1b8de61
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.")
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue