From 34c5fc2b7b2f1c3df6c8d32ad1eaf7937700d947 Mon Sep 17 00:00:00 2001 From: Levi Planelles Date: Tue, 2 Dec 2025 17:08:52 +0100 Subject: [PATCH] first commit --- __pycache__/proyecto.cpython-314.pyc | Bin 0 -> 65029 bytes proyecto.py | 1210 ++++++++++++++++++++++++++ 2 files changed, 1210 insertions(+) create mode 100644 __pycache__/proyecto.cpython-314.pyc create mode 100644 proyecto.py diff --git a/__pycache__/proyecto.cpython-314.pyc b/__pycache__/proyecto.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdc88df48409f26e082420e2227ba753793ac17a GIT binary patch literal 65029 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 KB/s + sent = (cur_net.bytes_sent - prev_net.bytes_sent) / 1024.0 + recv = (cur_net.bytes_recv - prev_net.bytes_recv) / 1024.0 + prev_net = cur_net + net_kb = (sent + recv) / 2.0 + + cpu_data = cpu_data[1:]+[cpu] + mem_data = mem_data[1:]+[mem] + net_data = net_data[1:]+[net_kb] + + line_cpu.set_ydata(cpu_data) + line_mem.set_ydata(mem_data) + line_net.set_ydata(net_data) + try: + canvas.draw() + except Exception: + # Si el canvas fue destruido, salir + return + try: + if win.winfo_exists(): + # guardar id para poder cancelarlo en on_close + nonlocal after_id + after_id = win.after(1000, update_plot) + except Exception: + return + + update_plot() + + +def open_text_editor(): + win = tk.Toplevel(root) + win.title("Editor de texto") + txt = tk.Text(win, wrap="word") + txt.pack(fill="both", expand=True) + + def save(): + path = fd.asksaveasfilename(defaultextension=".txt") + if path: + with open(path, "w", encoding="utf-8") as f: + f.write(txt.get("1.0", "end-1c")) + mb.showinfo("Guardado", "Archivo guardado") + + def open_file(): + path = fd.askopenfilename(filetypes=[("Text","*.txt;*.py;*.md"), ("All","*")]) + if path: + if os.path.isdir(path): + mb.showwarning("Abrir", "Selecciona un archivo, no una carpeta") + return + try: + with open(path, "r", encoding="utf-8") as f: + txt.delete("1.0", "end") + txt.insert("1.0", f.read()) + except Exception as e: + mb.showerror("Error", f"No se pudo leer el archivo:\n{e}") + + btns = tk.Frame(win) + ttk.Button(btns, text="Abrir", command=open_file, style="Secondary.TButton").pack(side="left") + ttk.Button(btns, text="Guardar", command=save, style="Accent.TButton").pack(side="left") + btns.pack() + + +def scrape_url(): + if not HAS_REQUESTS: + mb.showwarning("Dependencia", "requests/bs4 no están disponibles. Instálalos con pip install requests beautifulsoup4") + return + url = sd.askstring("Scraping", "Introduce la URL a scrapear:") + if not url: + return + try: + r = requests.get(url, timeout=10) + r.raise_for_status() + soup = BeautifulSoup(r.text, "html.parser") + # eliminar scripts, styles y noscript + for tag in soup(["script", "style", "noscript"]): + tag.decompose() + # intentar obtener título y meta description + title = soup.title.string.strip() if soup.title and soup.title.string else "" + meta_desc = "" + md = soup.find("meta", attrs={"name": "description"}) + if md and md.get("content"): + meta_desc = md.get("content").strip() + + # extraer texto visible, limpiar espacios + raw_text = soup.get_text(separator="\n") + # colapsar líneas en exceso y espacios + lines = [ln.strip() for ln in raw_text.splitlines()] + cleaned = "\n".join([ln for ln in lines if ln]) + + # preparar carpeta de salida `scrapping` en el directorio del script + base_dir = os.path.abspath(os.path.dirname(__file__)) + out_dir = os.path.join(base_dir, "scrapping") + try: + os.makedirs(out_dir, exist_ok=True) + except Exception: + pass + + # construir nombre de archivo seguro + from urllib.parse import urlparse + parsed = urlparse(url) + netloc = parsed.netloc or parsed.path.replace("/", "_") + safe_netloc = "".join([c if c.isalnum() else "_" for c in netloc])[:80] + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + txt_name = f"scrape_{safe_netloc}_{timestamp}.txt" + html_name = f"scrape_{safe_netloc}_{timestamp}.html" + txt_path = os.path.join(out_dir, txt_name) + html_path = os.path.join(out_dir, html_name) + + # escribir archivos + header = f"URL: {url}\nTitle: {title}\nMeta-Description: {meta_desc}\nTimestamp: {timestamp}\n\n" + try: + with open(txt_path, "w", encoding="utf-8") as f: + f.write(header) + f.write(cleaned) + except Exception as e: + mb.showwarning("Advertencia", f"No se pudo guardar el fichero txt:\n{e}") + + try: + with open(html_path, "w", encoding="utf-8") as f: + f.write(r.text) + except Exception: + pass + + # mostrar resultado reducido en una ventana y notificar fichero guardado + win = tk.Toplevel(root) + win.title(f"Scrape: {url}") + t = tk.Text(win, wrap="word") + t.insert("1.0", header + cleaned[:20000]) + t.pack(fill="both", expand=True) + + try: + mb.showinfo("Guardado", f"Contenido scrapado guardado en:\n{txt_path}") + except Exception: + pass + except Exception as e: + mb.showerror("Error", f"Falló scraping:\n{e}") + + +def fetch_weather_xabia(): + """Consulta la API de OpenWeatherMap para obtener el tiempo en Jávea (Alicante). + Pide al usuario la API key (se puede obtener en https://home.openweathermap.org/api_keys). + Actualiza la etiqueta central `center_status` con temperatura y muestra un cuadro informativo. + """ + # comprobar dependencia + if not HAS_REQUESTS: + mb.showwarning("Dependencia", "requests no está instalado. Instálalo con pip install requests") + return + + # Ruta para guardar la API key de forma persistente + cfg_dir = os.path.join(os.path.expanduser("~"), ".config", "proyecto") + key_file = os.path.join(cfg_dir, "openweather.key") + + api_key = None + # si existe fichero con key, usarla + try: + if os.path.exists(key_file): + with open(key_file, "r", encoding="utf-8") as fk: + k = fk.read().strip() + if k: + api_key = k + except Exception: + api_key = None + + # si no había key persistida, pedirla y guardarla + if not api_key: + api_key = sd.askstring("OpenWeatherMap API", "Introduce tu API Key de OpenWeatherMap:") + if not api_key: + return + try: + os.makedirs(cfg_dir, exist_ok=True) + with open(key_file, "w", encoding="utf-8") as fk: + fk.write(api_key.strip()) + except Exception: + # no crítico: continuar sin guardar + pass + + # Usar lat/lon para Jávea (Xàbia): lat=38.789166, lon=0.163055 + lat = 38.789166 + lon = 0.163055 + try: + url = "https://api.openweathermap.org/data/2.5/weather" + params = {"lat": lat, "lon": lon, "appid": api_key, "units": "metric", "lang": "es"} + r = requests.get(url, params=params, timeout=10) + r.raise_for_status() + data = r.json() + + temp = data.get("main", {}).get("temp") + desc = data.get("weather", [{}])[0].get("description", "") + humidity = data.get("main", {}).get("humidity") + wind = data.get("wind", {}).get("speed") + + info = f"Tiempo en Jávea, Alicante:\nTemperatura: {temp} °C\nCondición: {desc}\nHumedad: {humidity}%\nViento: {wind} m/s" + try: + mb.showinfo("Tiempo - Jávea", info) + except Exception: + pass + try: + center_status.config(text=f"Jávea: {temp}°C, {desc}") + except Exception: + pass + except requests.HTTPError as e: + # Manejo específico para 401 (Unauthorized) + try: + resp = getattr(e, 'response', None) + if resp is not None and resp.status_code == 401: + ans = mb.askyesno("Autenticación", "La API key no es válida (401 Unauthorized).\n¿Quieres borrar la key guardada y volver a introducirla?") + if ans: + try: + if os.path.exists(key_file): + os.remove(key_file) + except Exception: + pass + # reintentar: llamar recursivamente para pedir nueva key + try: + fetch_weather_xabia() + except Exception: + pass + else: + try: + mb.showerror("Error", "API key inválida. Revisa tu key en OpenWeatherMap.") + except Exception: + pass + return + except Exception: + pass + try: + mb.showerror("Error", f"Error al obtener datos: {e}") + except Exception: + pass + except Exception as e: + try: + mb.showerror("Error", f"Falló la consulta:\n{e}") + except Exception: + pass + + +def clear_openweather_key(): + """Borra la API key guardada de OpenWeather (si existe).""" + cfg_dir = os.path.join(os.path.expanduser("~"), ".config", "proyecto") + key_file = os.path.join(cfg_dir, "openweather.key") + try: + if os.path.exists(key_file): + os.remove(key_file) + mb.showinfo("Key eliminada", f"Se ha eliminado: {key_file}") + else: + mb.showinfo("Key", "No había ninguna key guardada") + except Exception as e: + try: + mb.showerror("Error", f"No se pudo borrar la key:\n{e}") + except Exception: + pass + + +def play_music_file(): + if not HAS_PYGAME: + # si pygame no está disponible, usaremos afplay como fallback + pass + path = fd.askopenfilename(filetypes=[("Audio","*.mp3;*.wav;*.midi;*.mid"), ("All","*")]) + if not path: + return + + def _play_with_pygame(p): + global music_playing, music_current + try: + with music_lock: + # detener cualquier reproducción previa + try: + pygame.mixer.music.stop() + except Exception: + pass + pygame.mixer.music.load(p) + pygame.mixer.music.play(-1) + music_current = p + music_playing = True + except Exception as e: + mb.showerror("Error", f"No se pudo reproducir con pygame:\n{e}") + + def _play_with_afplay(p): + global music_process, music_playing, music_current + try: + # detener proceso anterior + with music_lock: + if music_process is not None: + try: + music_process.kill() + except Exception: + pass + # iniciar afplay en background + music_process = subprocess.Popen(["afplay", p]) + music_current = p + music_playing = True + except Exception as e: + mb.showerror("Error", f"No se pudo reproducir con afplay:\n{e}") + + # arrancar en hilo para no bloquear la UI + def runner(p): + if HAS_PYGAME: + _play_with_pygame(p) + else: + _play_with_afplay(p) + + threading.Thread(target=runner, args=(path,), daemon=True).start() + + +def stop_music(): + """Detiene la reproducción iniciada por `play_music_file` (pygame o afplay).""" + global music_process, music_playing, music_current + with music_lock: + if HAS_PYGAME: + try: + pygame.mixer.music.stop() + except Exception: + pass + if music_process is not None: + try: + music_process.kill() + except Exception: + pass + music_process = None + music_playing = False + music_current = None + + +def set_alarm_minutes(): + mins = sd.askinteger("Alarma", "Avisar en cuántos minutos?", minvalue=1, maxvalue=1440) + if not mins: + return + + # si ya hay una alarma, cancelarla antes + try: + if alarm_control.get("event") is not None: + try: + alarm_control["event"].set() + except Exception: + pass + except Exception: + pass + + ev = Event() + end_ts = time.time() + mins * 60 + alarm_control["event"] = ev + alarm_control["end_ts"] = end_ts + + def alarm_worker(): + try: + while True: + if ev.is_set(): + # cancelada + try: + root.after(0, alarm_countdown_label.config, {"text": "Alarma cancelada"}) + except Exception: + pass + break + now_ts = time.time() + remaining = int(end_ts - now_ts) + if remaining <= 0: + # sonar alarma + try: + sound_path = "/System/Library/Sounds/Glass.aiff" + if HAS_PYGAME: + try: + s = pygame.mixer.Sound(sound_path) + s.play() + except Exception: + root.bell() + else: + subprocess.Popen(["afplay", sound_path]) + except Exception: + try: + root.bell() + except Exception: + pass + try: + root.after(0, mb.showinfo, "Alarma", f"Pasaron {mins} minutos") + except Exception: + pass + try: + root.after(0, alarm_countdown_label.config, {"text": "No hay alarma programada"}) + except Exception: + pass + break + + # actualizar etiqueta en hilo principal + try: + h = remaining // 3600 + mnt = (remaining % 3600) // 60 + s = remaining % 60 + text = f"Cuenta atrás: {h:02d}:{mnt:02d}:{s:02d}" + root.after(0, alarm_countdown_label.config, {"text": text}) + except Exception: + pass + + time.sleep(1) + finally: + # limpiar control + try: + alarm_control["event"] = None + alarm_control["end_ts"] = None + except Exception: + pass + + t = threading.Thread(target=alarm_worker, daemon=True) + alarm_control["thread"] = t + t.start() + + +def cancel_alarm(): + """Cancela la alarma programada (si existe).""" + try: + ev = alarm_control.get("event") + if ev is not None: + try: + ev.set() + except Exception: + pass + try: + alarm_countdown_label.config(text="Alarma cancelada") + except Exception: + pass + alarm_control["event"] = None + alarm_control["thread"] = None + alarm_control["end_ts"] = None + except Exception: + pass + + +def open_game_race(parent_canvas=None, num_racers=4, speed_mult=1.0): + """Ejecuta la carrera de camellos en un canvas dado. + Si no se proporciona canvas, abre un Toplevel (compatibilidad antigua). + num_racers: número de corredores + speed_mult: multiplicador de velocidad (>=0.1) + """ + if parent_canvas is None: + win = tk.Toplevel(root) + win.title("Carrera de camellos") + canvas = tk.Canvas(win, width=600, height=200, bg="white") + canvas.pack() + # si se crea un Toplevel, asegurar limpieza cuando se cierre + def _on_win_close(): + try: + stop_event.set() + except Exception: + pass + try: + win.destroy() + except Exception: + pass + # provisional — stop_event aún no creado; lo conectaremos más abajo estableciendo protocolo después + else: + canvas = parent_canvas + try: + canvas.delete("all") + canvas.config(bg="white") + except Exception: + # si el canvas no existe o fue destruido, no continuar + return + + # Calcular línea de meta en función del tamaño del canvas + try: + finish = canvas.winfo_width() - 50 + except Exception: + finish = 550 + if finish < 200: + finish = 550 + + camels = [] + colors = ["red", "blue", "green", "orange", "purple", "cyan", "magenta", "yellow"] + # limitar número de corredores + try: + n = max(1, min(int(num_racers), 12)) + except Exception: + n = 4 + for i in range(n): + y = 20 + i * 30 + color = colors[i % len(colors)] + rect = canvas.create_rectangle(10, y, 60, y + 25, fill=color) + camels.append(rect) + + # Control para anunciar ganador una sola vez + winner_lock = threading.Lock() + winner = {"index": None} + + lock = threading.Lock() + # evento para detener esta carrera + stop_event = Event() + race_stop_events[id(canvas)] = stop_event + + # si se creó win arriba, conectar el cierre a stop_event + try: + if 'win' in locals(): + win.protocol("WM_DELETE_WINDOW", _on_win_close) + except Exception: + pass + + def racer(item, idx): + while True: + if stop_event.is_set(): + return + try: + with lock: + try: + coords = canvas.coords(item) + except tk.TclError: + return + if not coords: + return + x1, y1, x2, y2 = coords + if x2 >= finish: + # Si aún no hay ganador, anunciarlo y resaltar + with winner_lock: + if winner["index"] is None: + winner["index"] = idx + 1 + try: + # resaltar ganador en dorado + root.after(0, lambda it=item: canvas.itemconfig(it, fill="#FFD700")) + except Exception: + pass + try: + root.after(0, mb.showinfo, "Ganador", f"¡Camello #{winner['index']} ha ganado!") + except Exception: + pass + # detener el resto de corredores + try: + stop_event.set() + except Exception: + pass + return + max_step = max(1, int(10 * float(speed_mult))) + step = random.randint(1, max_step) + try: + canvas.move(item, step, 0) + except tk.TclError: + return + except Exception: + return + time.sleep(random.uniform(0.05, 0.2)) + + for idx, r in enumerate(camels): + threading.Thread(target=racer, args=(r, idx), daemon=True).start() + + # Lanzar un watcher que elimina el evento cuando la carrera termina + def _watcher(): + try: + while True: + if stop_event.is_set(): + break + all_done = True + with lock: + for item in camels: + try: + coords = canvas.coords(item) + except tk.TclError: + # canvas destroyed -> stop + stop_event.set() + all_done = True + break + if coords and coords[2] < finish: + all_done = False + break + if all_done: + break + time.sleep(0.5) + finally: + try: + race_stop_events.pop(id(canvas), None) + except Exception: + pass + + threading.Thread(target=_watcher, daemon=True).start() + + +def launch_app(path): + """Abrir una aplicación en macOS usando `open` en un hilo separado.""" + def _run(): + if not os.path.exists(path): + # intentar con el nombre de la app si se pasó un nombre + try: + subprocess.run(["open", "-a", path], check=True) + return + except Exception as e: + mb.showerror("Error", f"No se encontró la aplicación:\n{path}\n{e}") + return + try: + subprocess.run(["open", path], check=True) + except Exception as e: + try: + subprocess.run(["open", "-a", path], check=True) + except Exception as e2: + mb.showerror("Error", f"No se pudo abrir la aplicación:\n{e}\n{e2}") + + threading.Thread(target=_run, daemon=True).start() + + +# Crear la ventana principal +root = tk.Tk() +root.title("Ventana Responsive") +root.geometry("1200x700") # Tamaño inicial (más ancho) + +# Tema y paleta básica +PALETTE = { + "bg_main": "#f5f7fa", + "sidebar": "#eef3f8", + "panel": "#ffffff", + "accent": "#2b8bd6", + "muted": "#7a8a99" +} +FONT_TITLE = ("Helvetica", 11, "bold") +FONT_NORMAL = ("Helvetica", 10) + +root.configure(bg=PALETTE["bg_main"]) +_style = ttk.Style(root) +try: + _style.theme_use("clam") +except Exception: + pass +_style.configure("Accent.TButton", background=PALETTE["accent"], foreground="white", font=FONT_NORMAL, padding=6) +_style.map("Accent.TButton", background=[('active', '#1e68b8')]) +_style.configure("Secondary.TButton", background="#eef6fb", foreground=PALETTE["accent"], font=FONT_NORMAL, padding=6) +_style.map("Secondary.TButton", background=[('active', '#e0f0ff')]) +_style.configure("TNotebook", background=PALETTE["bg_main"], tabposition='n') +_style.configure("TFrame", background=PALETTE["panel"]) + + +# Configurar la ventana principal para que sea responsive +root.columnconfigure(0, weight=0) # Columna izquierda, tamaño fijo +root.columnconfigure(1, weight=1) # Columna central, tamaño variable +root.columnconfigure(2, weight=0) # Columna derecha, tamaño fijo +root.rowconfigure(0, weight=1) # Fila principal, tamaño variable +root.rowconfigure(1, weight=0) # Barra de estado, tamaño fijo + +# Crear el menú superior +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_bar.add_cascade(label="Archivo", menu=file_menu) +menu_bar.add_cascade(label="Editar", menu=edit_menu) +menu_bar.add_cascade(label="Ayuda", menu=help_menu) + +root.config(menu=menu_bar) + +# Crear los frames laterales y el central +frame_izquierdo = tk.Frame(root, bg=PALETTE["sidebar"], width=220, highlightthickness=0) +frame_central = tk.Frame(root, bg=PALETTE["bg_main"]) +frame_derecho = tk.Frame(root, bg=PALETTE["sidebar"], width=260, highlightthickness=0) + +# Colocar los frames laterales y el central +frame_izquierdo.grid(row=0, column=0, sticky="ns") +frame_central.grid(row=0, column=1, sticky="nsew") +frame_derecho.grid(row=0, column=2, sticky="ns") + +# Configurar los tamaños fijos de los frames laterales +frame_izquierdo.grid_propagate(False) +frame_derecho.grid_propagate(False) + +# --- Contenido del sidebar izquierdo (secciones y botones) --- +left_title = tk.Label(frame_izquierdo, text="", bg=PALETTE["sidebar"]) +left_title.pack(pady=10) + +sec_acciones = tk.Label(frame_izquierdo, text="Acciones", bg=PALETTE["panel"], font=FONT_TITLE, anchor="w", padx=8) +sec_acciones.pack(fill="x", padx=8, pady=(8,2)) + +btn_extraer = ttk.Button(frame_izquierdo, text="Extraer datos", width=18, style="Secondary.TButton") +btn_navegar = ttk.Button(frame_izquierdo, text="Navegar", width=18, style="Secondary.TButton") +btn_buscar = ttk.Button(frame_izquierdo, text="Buscar API Google", width=18, style="Secondary.TButton") +btn_extraer.pack(pady=6, padx=8, fill='x') +btn_navegar.pack(pady=6, padx=8, fill='x') +btn_buscar.pack(pady=6, padx=8, fill='x') + +sec_apps = tk.Label(frame_izquierdo, text="Aplicaciones", bg=PALETTE["panel"], font=FONT_TITLE, anchor="w", padx=8) +sec_apps.pack(fill="x", padx=8, pady=(12,6)) + +btn_vscode = ttk.Button(frame_izquierdo, text="Visual Code", width=18, style="Accent.TButton") +btn_app2 = ttk.Button(frame_izquierdo, text="App2", width=18, style="Secondary.TButton") +btn_app3 = ttk.Button(frame_izquierdo, text="App3", width=18, style="Secondary.TButton") +btn_vscode.pack(pady=6, padx=8, fill='x') +btn_app2.pack(pady=6, padx=8, fill='x') +btn_app3.pack(pady=6, padx=8, fill='x') + +sec_batch = tk.Label(frame_izquierdo, text="Procesos batch", bg=PALETTE["panel"], font=FONT_TITLE, anchor="w", padx=8) +sec_batch.pack(fill="x", padx=8, pady=(12,6)) + +btn_backup = ttk.Button(frame_izquierdo, text="Copias de seguridad", width=18, style="Secondary.TButton") +btn_backup.pack(pady=6, padx=8, fill='x') +# --- Contenido del sidebar derecho (chat y lista de alumnos) --- +chat_title = tk.Label(frame_derecho, text="Chat", font=("Helvetica", 14, "bold"), bg=PALETTE["sidebar"]) +chat_title.pack(pady=(8,8)) + +msg_label = tk.Label(frame_derecho, text="Mensaje", bg=PALETTE["sidebar"], font=FONT_NORMAL) +msg_label.pack(padx=8, anchor="w") + +msg_text = tk.Text(frame_derecho, height=6, width=26, bd=0, relief="flat") +msg_text.pack(padx=8, pady=(6,8), fill="x") + +send_btn = ttk.Button(frame_derecho, text="Enviar", style="Accent.TButton") +send_btn.pack(padx=8, pady=(0,12)) + +alumnos_label = tk.Label(frame_derecho, text="Alumnos", bg=PALETTE["sidebar"], font=FONT_TITLE) +alumnos_label.pack(padx=8, anchor="w") + +# Frame con scrollbar para la lista de alumnos +alumnos_frame = tk.Frame(frame_derecho) +alumnos_frame.pack(fill="both", expand=True, padx=8, pady=6) + +canvas = tk.Canvas(alumnos_frame, borderwidth=0, highlightthickness=0, bg="white") +scrollbar = tk.Scrollbar(alumnos_frame, orient="vertical", command=canvas.yview) +inner = tk.Frame(canvas, bg="white") +inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) +canvas.create_window((0, 0), window=inner, anchor="nw") +canvas.configure(yscrollcommand=scrollbar.set) +canvas.pack(side="left", fill="both", expand=True) +scrollbar.pack(side="right", fill="y") + +# Añadir algunos alumnos de ejemplo +for n in range(1, 6): + a_frame = tk.Frame(inner, bg="white", bd=1, relief="groove") + tk.Label(a_frame, text=f"Alumno {n}", font=("Helvetica", 12, "bold"), bg="white").pack(anchor="w") + tk.Label(a_frame, text="Lorem ipsum dolor sit amet, consectetur...", bg="white", wraplength=160, justify="left").pack(anchor="w", pady=(2,6)) + a_frame.pack(fill="x", pady=4) + + + +music_label = tk.Label(frame_derecho, text="Reproductor música", bg="#dcdcdc") +music_label.pack(fill="x", padx=8, pady=(6,8)) + +# Botones / comandos vinculados +btn_navegar.config(command=launch_browser_prompt) +# El botón de copias ahora pide un archivo o carpeta y lo copia a ./backup +btn_backup.config(command=backup_ui) +# Abrir Visual Studio Code (ruta absoluta en macOS) +btn_vscode.config(command=lambda: launch_app("/Applications/Visual Studio Code.app")) +btn_app2.config(command=open_resource_window) +btn_app3.config(command=open_game_race) +btn_extraer.config(command=scrape_url) +btn_buscar.config(command=fetch_weather_xabia) + # refresh button removed (was duplicated per alumno) + +# Los controles de alarma y reproducción de música están disponibles en la pestaña "Enlaces" + +# Enviar mensaje (simulado) +def send_message(): + text = msg_text.get("1.0", "end-1c").strip() + if not text: + mb.showwarning("Mensaje", "El mensaje está vacío") + return + mb.showinfo("Mensaje", "Mensaje enviado (simulado)") + msg_text.delete("1.0", "end") + +send_btn.config(command=send_message) + +# Dividir el frame central en dos partes (superior variable e inferior fija) +frame_central.rowconfigure(0, weight=1) # Parte superior, tamaño variable +frame_central.rowconfigure(1, weight=0) # Parte inferior, tamaño fijo +frame_central.columnconfigure(0, weight=1) # Ocupa toda la anchura + +# Crear subframes dentro del frame central +frame_superior = tk.Frame(frame_central, bg="lightyellow") +frame_inferior = tk.Frame(frame_central, bg="lightgray", height=100) + +# Colocar los subframes dentro del frame central +frame_superior.grid(row=0, column=0, sticky="nsew") +frame_inferior.grid(row=1, column=0, sticky="ew") + +# Fijar el tamaño de la parte inferior +frame_inferior.grid_propagate(False) + +# Añadir texto informativo en la parte inferior central +info_label = tk.Label(frame_inferior, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", + bg=PALETTE["panel"], anchor="w", justify="left", padx=12, font=FONT_NORMAL) +info_label.pack(fill="both", expand=True, padx=8, pady=8) + +# Crear la barra de estado como contenedor (Frame) +barra_estado = tk.Frame(root, bg="lightgray") +barra_estado.grid(row=1, column=0, columnspan=3, sticky="ew") + +# Notebook para las pestañas +style = ttk.Style() +style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold")) +notebook = ttk.Notebook(frame_superior, style="CustomNotebook.TNotebook") +notebook.pack(fill="both", expand=True, padx=6, pady=6) + +# Crear seis solapas con nombres definidos +tab_resultados = ttk.Frame(notebook) +tab_navegador = ttk.Frame(notebook) +tab_correos = ttk.Frame(notebook) +tab_tareas = ttk.Frame(notebook) +tab_alarmas = ttk.Frame(notebook) +tab_enlaces = ttk.Frame(notebook) + +notebook.add(tab_resultados, text="Resultados", padding=8) +notebook.add(tab_navegador, text="Navegador", padding=8) +notebook.add(tab_correos, text="Correos", padding=8) +notebook.add(tab_tareas, text="Tareas", padding=8) +notebook.add(tab_alarmas, text="Alarmas", padding=8) +notebook.add(tab_enlaces, text="Enlaces", padding=8) + +# --- Contenido básico de cada solapa --- +# Resultados: canvas del juego y botón para iniciar la carrera +res_top = tk.Frame(tab_resultados) +res_top.pack(fill="both", expand=True) +res_controls = tk.Frame(tab_resultados, height=40) +res_controls.pack(fill="x") +res_canvas = tk.Canvas(res_top, width=800, height=300, bg="white") +res_canvas.pack(fill="both", expand=True, padx=8, pady=8) +# Controles: iniciar, número de corredores, velocidad y detener +start_race_btn = ttk.Button(res_controls, text="Iniciar Carrera", style="Accent.TButton") +start_race_btn.pack(side="left", padx=8, pady=6) +tk.Label(res_controls, text="Corredores:").pack(side="left", padx=(10,2)) +num_spin = tk.Spinbox(res_controls, from_=1, to=12, width=4) +num_spin.pack(side="left", padx=2) +tk.Label(res_controls, text="Velocidad:").pack(side="left", padx=(10,2)) +speed_scale = tk.Scale(res_controls, from_=0.5, to=3.0, resolution=0.1, orient="horizontal", length=140) +speed_scale.set(1.0) +speed_scale.pack(side="left", padx=2) +stop_race_btn = ttk.Button(res_controls, text="Detener Carrera", style="Secondary.TButton") +stop_race_btn.pack(side="left", padx=8) + +# Enlazar el botón para ejecutar la carrera dentro del canvas de la solapa Resultados +def _start_from_ui(): + try: + n = int(num_spin.get()) + except Exception: + n = 4 + try: + sp = float(speed_scale.get()) + except Exception: + sp = 1.0 + open_game_race(res_canvas, num_racers=n, speed_mult=sp) + +def _stop_from_ui(): + ev = race_stop_events.get(id(res_canvas)) + if ev is not None: + try: + ev.set() + except Exception: + pass + try: + res_canvas.delete("all") + except Exception: + pass + +start_race_btn.config(command=_start_from_ui) +stop_race_btn.config(command=_stop_from_ui) + +# Navegador: entrada de URL y botón +nav_frame = tk.Frame(tab_navegador) +nav_frame.pack(fill="both", expand=True, padx=8, pady=8) +url_entry = tk.Entry(nav_frame) +url_entry.insert(0, "https://www.google.com") +url_entry.pack(fill="x", side="left", expand=True, padx=(0,8)) +open_url_btn = ttk.Button(nav_frame, text="Abrir", command=lambda: threading.Thread(target=launch_browser, args=(url_entry.get(),), daemon=True).start(), style="Accent.TButton") +open_url_btn.pack(side="right") + +# Correos: cuadro de chat simple (simulado) +cor_frame = tk.Frame(tab_correos) +cor_frame.pack(fill="both", expand=True, padx=8, pady=8) +cor_msg_text = tk.Text(cor_frame, height=12) +cor_msg_text.pack(fill="both", expand=True) +cor_send_btn = ttk.Button(cor_frame, text="Enviar", width=12, style="Accent.TButton") +cor_send_btn.pack(pady=(6,0)) + +def correos_send(): + text = cor_msg_text.get("1.0", "end-1c").strip() + if not text: + mb.showwarning("Mensaje", "El mensaje está vacío") + return + mb.showinfo("Mensaje", "Mensaje enviado (simulado)") + cor_msg_text.delete("1.0", "end") + +cor_send_btn.config(command=correos_send) + +# Tareas: editor simple embebido +task_frame = tk.Frame(tab_tareas) +task_frame.pack(fill="both", expand=True, padx=8, pady=8) +task_text = tk.Text(task_frame, wrap="word") +task_text.pack(fill="both", expand=True) +task_btns = tk.Frame(tab_tareas) +task_btns.pack(fill="x") + +def task_open(): + path = fd.askopenfilename(filetypes=[("Text","*.txt;*.py;*.md"), ("All","*")]) + if not path: + return + if os.path.isdir(path): + mb.showwarning("Abrir", "Selecciona un archivo, no una carpeta") + return + try: + with open(path, "r", encoding="utf-8") as f: + task_text.delete("1.0", "end") + task_text.insert("1.0", f.read()) + except Exception as e: + mb.showerror("Error", f"No se pudo leer el archivo:\n{e}") + +def task_save(): + path = fd.asksaveasfilename(defaultextension=".txt") + if not path: + return + try: + with open(path, "w", encoding="utf-8") as f: + f.write(task_text.get("1.0", "end-1c")) + mb.showinfo("Guardado", "Archivo guardado") + except Exception as e: + mb.showerror("Error", f"No se pudo guardar el archivo:\n{e}") + + ttk.Button(task_btns, text="Abrir", command=task_open, style="Secondary.TButton").pack(side="left", padx=4, pady=6) + ttk.Button(task_btns, text="Guardar", command=task_save, style="Accent.TButton").pack(side="left", padx=4, pady=6) + +# Alarmas: usar set_alarm_minutes (ya existente) +alarm_frame = tk.Frame(tab_alarmas) +alarm_frame.pack(fill="both", expand=True, padx=8, pady=8) +ttk.Button(alarm_frame, text="Programar alarma", command=set_alarm_minutes, style="Accent.TButton").pack(pady=8) + +# Label de cuenta regresiva y botón cancelar +alarm_countdown_label = tk.Label(alarm_frame, text="No hay alarma programada", font=FONT_TITLE, bg=PALETTE["panel"], fg=PALETTE["muted"], padx=8, pady=6) +alarm_countdown_label.pack(pady=(6,8), fill="x") +ttk.Button(alarm_frame, text="Cancelar alarma", command=lambda: threading.Thread(target=lambda: cancel_alarm(), daemon=True).start(), style="Secondary.TButton").pack() + +# Enlaces: botones para abrir apps y utilidades +links_frame = tk.Frame(tab_enlaces) +links_frame.pack(fill="both", expand=True, padx=8, pady=8) +ttk.Button(links_frame, text="Abrir Visual Studio Code", command=lambda: launch_app("/Applications/Visual Studio Code.app"), style="Accent.TButton").pack(fill="x", pady=4) +ttk.Button(links_frame, text="Abrir Spotify", command=lambda: launch_app("/Applications/Spotify.app"), style="Secondary.TButton").pack(fill="x", pady=4) +ttk.Button(links_frame, text="Mostrar recursos (matplotlib)", command=open_resource_window, style="Secondary.TButton").pack(fill="x", pady=4) +ttk.Button(links_frame, text="Reproducir música (archivo)", command=play_music_file, style="Secondary.TButton").pack(fill="x", pady=4) +ttk.Button(links_frame, text="Detener música", command=stop_music, style="Secondary.TButton").pack(fill="x", pady=4) +ttk.Button(links_frame, text="Borrar OpenWeather Key", command=clear_openweather_key, style="Secondary.TButton").pack(fill="x", pady=4) + +# Barra de estado +# Dividir la barra de estado en 4 labels + + +# Usar pack para alinear los labels horizontalmente + + + +# Secciones en la barra de estado: izquierda, centro y derecha +left_status = tk.Label(barra_estado, text="Correos sin leer 🔄", bg="#f0f0f0", anchor="w", padx=8) +center_status = tk.Label(barra_estado, text="Temperatura local: -- °C", bg="#f0f0f0", anchor="center") +label_fecha_hora = tk.Label(barra_estado, text="Cargando fecha...", font=("Helvetica", 12), bd=1, fg="blue", relief="sunken", anchor="e", padx=10) + +left_status.pack(side="left", fill="x", expand=True) +center_status.pack(side="left", fill="x", expand=True) +label_fecha_hora.pack(side="right") + +# Iniciar hilo para actualizar la fecha/hora +update_thread = threading.Thread(target=update_time, args=(label_fecha_hora,)) +update_thread.daemon = True +update_thread.start() + + +# Hilo que monitoriza tráfico de red y actualiza la etiqueta central en KB/s +def network_monitor(label_widget): + try: + prev = psutil.net_io_counters() + except Exception: + return + while True: + time.sleep(1) + cur = psutil.net_io_counters() + sent = (cur.bytes_sent - prev.bytes_sent) / 1024.0 + recv = (cur.bytes_recv - prev.bytes_recv) / 1024.0 + prev = cur + text = f"Tráfico - In: {recv:.1f} KB/s Out: {sent:.1f} KB/s" + try: + label_widget.after(0, label_widget.config, {"text": text}) + except Exception: + break + + +net_thread = threading.Thread(target=network_monitor, args=(center_status,)) +net_thread.daemon = True +net_thread.start() + +# Ejecución de la aplicación +root.mainloop() \ No newline at end of file