Implemetacion de 2 solapas el tiempo y musica

This commit is contained in:
Santiago Parra 2024-12-13 19:52:23 +01:00
parent 9d1bbdaa85
commit 19b77c2349
49 changed files with 595 additions and 91 deletions

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app" vcs="Git" />
</component>
</project>

View File

@ -16,17 +16,34 @@ class ConfigMgr:
def __load_config(self):
if os.path.exists('config.ini'):
self.config.read('config.ini')
self.__check_config()
else:
print("Config file not found, creating default config file")
with open('config.ini', 'w') as f:
self.__write_default_config(f)
self.config.read('config.ini')
def __check_config(self):
print("Checking config file")
if "Chat" not in self.config:
self.config["Chat"] = {
"server": "http://localhost:2020",
"name": "User"
}
if "Weather" not in self.config:
self.config["Weather"] = {
"city": "Denia, Alicante"
}
def __write_default_config(self, file):
chat_config = ("[Chat]\n"
"server=http://localhost:2020\n"
"name=User\n")
file.write(chat_config)
weather_config = ("[Weather]\n"
"city=Denia, Alicante\n")
file.write(chat_config + weather_config)
def display_config_window(self):
if (self.config_window is None
@ -43,6 +60,7 @@ class ConfigMgr:
notebook = ttk.Notebook(config_window)
notebook.pack(expand=True, fill="both")
# Chat Config Tab
chat_tab = ttk.Frame(notebook)
notebook.add(chat_tab, text="Chat Config")
@ -66,6 +84,20 @@ class ConfigMgr:
chat_name_input = tk.Entry(chat_tab, textvariable=self.chat_name_variable)
chat_name_input.pack()
# Weather Config Tab
weather_tab = ttk.Frame(notebook)
notebook.add(weather_tab, text="Weather Config")
weather_city_label = tk.Label(weather_tab, text="City")
weather_city_label.pack()
self.weather_city_variable = tk.StringVar()
try:
self.weather_city_variable.set(self.config["Weather"]["city"])
except KeyError:
self.weather_city_variable.set("")
weather_city_input = tk.Entry(weather_tab, textvariable=self.weather_city_variable)
weather_city_input.pack()
self.save_button = tk.Button(config_window, text="Save", command=self.save_config)
self.save_button.pack(pady=10)
@ -74,6 +106,9 @@ class ConfigMgr:
def save_config(self):
self.config["Chat"] = {"server": self.chat_server_variable.get(),
"name": self.chat_name_variable.get()}
self.config["Weather"] = {"city": self.weather_city_variable.get()}
with open('config.ini', 'w') as configfile:
self.config.write(configfile)

View File

@ -2,7 +2,7 @@ import threading
import time
import flask
from werkzeug.serving import make_server
import logging
class Server:
"""
@ -30,6 +30,11 @@ class Server:
self.server_thread.start()
self.watcher_thread.start()
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
self.flask.logger.setLevel(logging.ERROR)
# Message initialization
self.message_id = 0
self.messages = self.__init_messages()

View File

@ -1,2 +1,2 @@
1|Santi|Hola como estas?
2|Santi|Andres no me copies el TO DO
1|User|Hola
2|feo|hola

View File

@ -1,4 +1,7 @@
[Chat]
server = http://localhost:2020
name = Santi
name = feo
[Weather]
city = Denia

View File

@ -7,124 +7,163 @@ from app.chat_server.Server import Server
from app.widgets import ClockLabel
from app.ConfigMgr import ConfigMgr
from app.widgets.ChatTab import ChatTab
from app.widgets.MusicDownloadTab import MusicDownloadTab
from app.widgets.MusicPlayerTab import MusicPlayerTab
from app.widgets.TodoTab import TodoTab
from app.widgets.UsageLabels import CPULabel, RAMLabel, BatteryLabel, NetworkLabel
from app.widgets.WeatherTab import WeatherTab
# Evento para detener threads de manera segura
stop_event = threading.Event()
def on_closing():
"""Gestiona el cierre de la aplicación de manera segura."""
# Detiene todos los threads relacionados con stop_event
# Kill all threads that are linked to the stop_event
# This ensures that execution is thread-safe
stop_event.set()
# Cierra la ventana principal
# Close the main window
root.quit()
root.destroy()
def on_config_changed():
"""Actualiza la configuración del servidor y nombre de usuario en el chat."""
chat_frame.change_server_url(config_manager.config["Chat"]["server"])
chat_frame.change_sender_name(config_manager.config["Chat"]["name"])
weather_tab.changeCity(config_manager.config["Weather"]["city"])
# Crea la ventana principal
# Create the main window
root = tk.Tk()
root.title("Responsive Window") # Título de la ventana
root.geometry("1150x700") # Tamaño inicial de la ventana
root.title("Responsive Window")
root.geometry("1150x700")
# Inicializa el gestor de configuración
config_manager = ConfigMgr(root, config_changed_listener=on_config_changed)
# Inicializa el servidor de chat
server = Server(host="localhost", port=2020, stop_event=stop_event)
# Configura la ventana principal para que sea responsive
root.columnconfigure(0, weight=0) # Columna izquierda con tamaño fijo
root.columnconfigure(1, weight=1) # Columna central ajustable
root.columnconfigure(2, weight=0) # Columna derecha con tamaño fijo
root.rowconfigure(0, weight=1) # Fila principal ajustable
root.rowconfigure(1, weight=0) # Barra de estado con tamaño fijo
# Configure the main window to be responsive
root.columnconfigure(0, weight=0)
root.columnconfigure(1, weight=1)
root.columnconfigure(2, weight=0)
root.rowconfigure(0, weight=1)
root.rowconfigure(1, weight=0)
# Crea el menú superior
# Create the top menu
menu_bar = Menu(root)
# Menú Archivo
file_menu = Menu(menu_bar, tearoff=0)
file_menu.add_command(label="New") # Comando Nuevo
file_menu.add_command(label="Open") # Comando Abrir
file_menu.add_separator() # Separador visual
file_menu.add_command(label="Config", command=config_manager.display_config_window) # Configuración
file_menu.add_command(label="Exit", command=on_closing) # Salir
file_menu.add_command(label="New")
file_menu.add_command(label="Open")
file_menu.add_separator()
file_menu.add_command(label="Config", command=config_manager.display_config_window)
file_menu.add_command(label="Exit", command=on_closing)
# Menú Edición
edit_menu = Menu(menu_bar, tearoff=0)
edit_menu.add_command(label="Copy") # Comando Copiar
edit_menu.add_command(label="Paste") # Comando Pegar
edit_menu.add_command(label="Copy")
edit_menu.add_command(label="Paste")
# Añade los menús al menú principal
menu_bar.add_cascade(label="File", menu=file_menu)
menu_bar.add_cascade(label="Edit", menu=edit_menu)
# Asigna el menú a la ventana principal
root.config(menu=menu_bar)
# Crea los marcos laterales y central
frame_left = tk.Frame(root, bg="lightblue", width=200) # Marco izquierdo
frame_center = tk.Frame(root, bg="white") # Marco central
chat_frame = ChatTab(root, chat_server_url=config_manager.config["Chat"]["server"],
sender_name=config_manager.config["Chat"]["name"],
stop_event=stop_event, width=200, bg="lightgreen") # Marco derecho para el chat
# Create the side and central frames
frame_left = tk.Frame(root, bg="lightblue", width=300)
frame_center = tk.Frame(root, bg="white")
frame_right = tk.Frame(root, bg="lightgreen", width=300)
# Coloca los marcos en la cuadrícula
frame_left.grid(row=0, column=0, sticky="ns") # Marco izquierdo
frame_center.grid(row=0, column=1, sticky="nsew") # Marco central
chat_frame.grid(row=0, column=2, sticky="ns") # Marco derecho
# Place the side and central frames
frame_left.grid(row=0, column=0, sticky="ns")
frame_center.grid(row=0, column=1, sticky="nsew")
frame_right.grid(row=0, column=2, sticky="ns")
# Configura tamaños fijos para los marcos laterales
# Configure the fixed sizes of the side frames
frame_left.grid_propagate(False)
chat_frame.grid_propagate(False)
frame_right.grid_propagate(False)
# Divide el marco central en dos partes (superior ajustable e inferior fijo)
frame_center.rowconfigure(0, weight=1) # Parte superior ajustable
frame_center.rowconfigure(1, weight=0) # Parte inferior fija
frame_center.columnconfigure(0, weight=1) # Ancho ajustable
# Configure the left frame to have three rows
frame_left.rowconfigure(0, weight=1)
frame_left.rowconfigure(1, weight=0)
frame_left.rowconfigure(2, weight=0)
# Crea sub-marcos dentro del marco central
frame_top = tk.Frame(frame_center, bg="lightyellow") # Parte superior
frame_bottom = tk.Frame(frame_center, bg="lightgray", height=100) # Parte inferior
# Add weather tab to the left frame, occupying 1/3 of the height
weather_tab = WeatherTab(frame_left, stop_event=stop_event,
city=config_manager.config["Weather"]["city"], refresh_rate=300)
weather_tab.grid(row=0, column=0, sticky="nsew")
# Coloca los sub-marcos en el marco central
frame_top.grid(row=0, column=0, sticky="nsew") # Parte superior
frame_bottom.grid(row=1, column=0, sticky="ew") # Parte inferior
# Adjust the remaining rows to occupy the rest of the space
frame_left.rowconfigure(1, weight=2)
frame_left.rowconfigure(2, weight=2)
# Fija el tamaño de la parte inferior
# Divide the central frame into two parts (top variable and bottom fixed)
frame_center.rowconfigure(0, weight=1)
frame_center.rowconfigure(1, weight=0)
frame_center.columnconfigure(0, weight=1)
# Create subframes within the central frame
frame_top = tk.Frame(frame_center, bg="lightyellow")
frame_bottom = tk.Frame(frame_center, bg="lightgray", height=100)
# Place the subframes within the central frame
frame_top.grid(row=0, column=0, sticky="nsew")
frame_bottom.grid(row=1, column=0, sticky="ew")
# Fix the size of the bottom part
frame_bottom.grid_propagate(False)
# Crea la barra de estado
status_bar = tk.Label(root, text="Status bar", bg="lightgray", anchor="w") # Barra de estado
# Create the status bar
status_bar = tk.Label(root, text="Status bar", bg="lightgray", anchor="w")
status_bar.grid(row=1, column=0, columnspan=3, sticky="ew")
# Configura un cuaderno (notebook) para widgets
# Notebook for widgets
style = ttk.Style()
style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold"))
notebook = ttk.Notebook(frame_top, style="CustomNotebook.TNotebook")
notebook.pack(fill="both", expand=True)
# Añade etiquetas de uso del sistema
label_cpu = CPULabel(status_bar, bg="lightgreen", font=("Helvetica", 10), relief="groove", anchor="center", width=10, stop_event=stop_event) # Uso de CPU
label_ram = RAMLabel(status_bar, bg="lightcoral", font=("Helvetica", 10), relief="groove", anchor="center", width=10, stop_event=stop_event) # Uso de RAM
label_battery = BatteryLabel(status_bar, bg="lightblue", font=("Helvetica", 10), relief="groove", anchor="center", width=20, stop_event=stop_event) # Batería
label_net = NetworkLabel(status_bar, text="Network", bg="lightpink", font=("Helvetica", 10), relief="groove", anchor="center", width=20, stop_event=stop_event) # Red
label_time = ClockLabel(status_bar, font=("Helvetica", 12), bd=1, fg="darkblue", relief="sunken", anchor="center", width=20, stop_event=stop_event) # Reloj
# Add the tabs to the notebook
music_download_tab = MusicDownloadTab(notebook, stop_event=stop_event)
music_download_tab.pack(fill="both", expand=True)
notebook.add(music_download_tab, text="Music Download")
# Add the TodoTab to the notebook
todo_tab = TodoTab(notebook, stop_event=stop_event)
todo_tab.pack(fill="both", expand=True)
notebook.add(todo_tab, text="Todo List")
# Create the chat and music player frames within the right frame
frame_chat = tk.Frame(frame_right, bg="lightgreen")
frame_music_player = tk.Frame(frame_right)
# Place the chat and music player frames within the right frame
frame_chat.grid(row=0, column=0, sticky="nsew")
frame_music_player.grid(row=1, column=0, sticky="nsew")
# Configure the right frame to be responsive
frame_right.rowconfigure(0, weight=3)
frame_right.rowconfigure(1, weight=1)
frame_right.columnconfigure(0, weight=1)
# Create and place the chat frame and music player tab
chat_frame = ChatTab(frame_chat, chat_server_url=config_manager.config["Chat"]["server"],
sender_name=config_manager.config["Chat"]["name"],
stop_event=stop_event, width=200, bg="lightgreen", refresh_rate=1)
chat_frame.pack(fill="both", expand=True)
music_player = MusicPlayerTab(frame_music_player, stop_event=stop_event, refresh_rate=5)
music_player.pack(fill="both", expand=True)
label_cpu = CPULabel(status_bar, bg="lightgreen", font=("Helvetica", 10), relief="groove", anchor="center", width=10, stop_event=stop_event, refresh_rate=1)
label_ram = RAMLabel(status_bar, bg="lightcoral", font=("Helvetica", 10), relief="groove", anchor="center", width=10, stop_event=stop_event, refresh_rate=3)
label_battery = BatteryLabel(status_bar, bg="lightblue", font=("Helvetica", 10), relief="groove", anchor="center", width=20, stop_event=stop_event, refresh_rate=10)
label_net = NetworkLabel(status_bar, text="Network", bg="lightpink", font=("Helvetica", 10), relief="groove", anchor="center", width=20, stop_event=stop_event, refresh_rate=5)
label_time = ClockLabel(status_bar, font=("Helvetica", 12), bd=1, fg="darkblue", relief="sunken", anchor="center", width=20, stop_event=stop_event, refresh_rate=0.5)
# Coloca las etiquetas en la barra de estado
label_cpu.pack(side="left", fill="both", expand=True)
label_ram.pack(side="left", fill="both", expand=True)
label_battery.pack(side="left", fill="both", expand=True)
label_net.pack(side="left", fill="both", expand=True)
label_time.pack(side="right", fill="both", expand=True)
# Configura la acción para el cierre de la ventana
root.protocol("WM_DELETE_WINDOW", on_closing)
# Inicia la aplicación
# Run the application
root.mainloop()

View File

@ -1,13 +1,28 @@
beautifulsoup4==4.12.3
blinker==1.9.0
cairocffi==1.7.1
CairoSVG==2.7.1
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.4.0
click==8.1.7
cssselect2==0.7.0
defusedxml==0.7.1
Flask==3.1.0
geographiclib==2.0
geopy==2.4.1
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
psutil==6.1.0
pycparser==2.22
pygame==2.6.1
requests==2.32.3
soupsieve==2.6
tinycss2==1.4.0
urllib3==2.2.3
weatherkit==1.1.1
webencodings==0.5.1
Werkzeug==3.1.3
yt-dlp==2024.12.6

0
app/todo.list Normal file
View File

View File

@ -26,7 +26,6 @@ class ChatTab(ThreadedTab):
self.conn = True
except requests.ConnectionError:
self.disconnected()
time.sleep(1)
def build(self):
# Create the main frame for the chat interface

View File

@ -11,6 +11,4 @@ class ClockLabel(ThreadedLabel):
time_str = now.strftime("%H:%M:%S")
date_str = now.strftime("%Y-%m-%d")
label_text = f"{day_of_week}, {date_str} - {time_str}"
try: self.config(text=label_text)
except RuntimeError: pass
time.sleep(0.5)
self.config(text=label_text)

View File

@ -0,0 +1,71 @@
import os
import time
from tkinter import Tk, Frame, Entry, Button, Label, StringVar, messagebox
from yt_dlp import YoutubeDL
from urllib.parse import urlparse, parse_qs, urlunparse, urlencode
from app.widgets.abc import ThreadedTab
class MusicDownloadTab(ThreadedTab):
def __init__(self, root: Frame | Tk, **kwargs):
self.download_url = StringVar()
self.status = StringVar()
self.download_queue = []
super().__init__(root, **kwargs)
def build(self):
# Create the main frame for the download interface
self.download_frame = Frame(self)
self.download_frame.pack(fill="both", expand=True)
# Create the input field for the YouTube link
self.url_entry = Entry(self.download_frame, textvariable=self.download_url)
self.url_entry.pack(fill="x", padx=5, pady=5)
# Create the download button
self.download_button = Button(self.download_frame, text="Download", command=self.queue_download)
self.download_button.pack(padx=5, pady=5)
# Create the status label
self.status_label = Label(self.download_frame, textvariable=self.status)
self.status_label.pack(fill="x", padx=5, pady=5)
def queue_download(self):
url = self.download_url.get()
if url:
parsed_url = urlparse(url)
query_params = parse_qs(parsed_url.query)
if 'list' in query_params:
response = messagebox.askyesno("Download Playlist", "Do you want to download the whole playlist?")
if not response:
query_params.pop('list', None)
query_params.pop('index', None)
new_query = urlencode(query_params, doseq=True)
url = urlunparse(parsed_url._replace(query=new_query))
self.download_queue.append(url)
self.status.set("Queued for download")
def task(self):
if self.download_queue:
url = self.download_queue.pop(0)
self.download_music(url)
time.sleep(1)
def download_music(self, url):
try:
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': os.path.join("music", '%(title)s.%(ext)s'),
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192',
}],
}
with YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
self.status.set("Downloaded successfully")
except Exception as e:
print(e)
self.status.set(f"Error: {str(e)}")
time.sleep(1)

View File

@ -0,0 +1,82 @@
import os
import time
import threading
import pygame
from tkinter import Tk, Frame, Button, Label, StringVar
from app.widgets.abc import ThreadedTab
class MusicPlayerTab(ThreadedTab):
def __init__(self, root: Frame | Tk, **kwargs):
self.music_dir = "music"
self.music_files = []
self.current_index = 0
self.is_playing = False
self.current_song = StringVar()
super().__init__(root, **kwargs)
pygame.mixer.init()
def build(self):
# Create the main frame for the music player interface
self.player_frame = Frame(self)
self.player_frame.pack(fill="both", expand=True)
# Create the label to display the current song
self.song_label = Label(self.player_frame, textvariable=self.current_song, anchor="center", wraplength=200)
self.song_label.pack(padx=5, pady=5)
# Create the control buttons frame
self.controls_frame = Frame(self.player_frame)
self.controls_frame.pack(expand=True)
# Create the control buttons
self.prev_button = Button(self.controls_frame, text="<", command=self.previous_song)
self.prev_button.pack(side="left", padx=5)
self.play_pause_button = Button(self.controls_frame, text="|| / >", command=self.play_pause_music)
self.play_pause_button.pack(side="left", padx=5)
self.next_button = Button(self.controls_frame, text=">", command=self.next_song)
self.next_button.pack(side="left", padx=5)
self.load_music()
def load_music(self):
if not os.path.exists(self.music_dir):
os.makedirs(self.music_dir)
self.music_files = [f for f in os.listdir(self.music_dir) if f.endswith('.mp3')]
if self.music_files:
self.current_song.set(self.music_files[self.current_index])
def play_pause_music(self):
if self.is_playing:
pygame.mixer.music.pause()
self.is_playing = False
else:
if pygame.mixer.music.get_busy():
pygame.mixer.music.unpause()
else:
pygame.mixer.music.load(os.path.join(self.music_dir, self.music_files[self.current_index]))
pygame.mixer.music.play()
self.is_playing = True
def next_song(self):
self.current_index = (self.current_index + 1) % len(self.music_files)
self.current_song.set(self.music_files[self.current_index])
pygame.mixer.music.load(os.path.join(self.music_dir, self.music_files[self.current_index]))
pygame.mixer.music.play()
self.is_playing = True
def previous_song(self):
self.current_index = (self.current_index - 1) % len(self.music_files)
self.current_song.set(self.music_files[self.current_index])
pygame.mixer.music.load(os.path.join(self.music_dir, self.music_files[self.current_index]))
pygame.mixer.music.play()
self.is_playing = True
def task(self):
self.load_music()

View File

@ -0,0 +1,59 @@
import os
import time
from tkinter import Tk, Frame, Entry, Button, Label, Listbox, StringVar, messagebox
from app.widgets.abc import ThreadedTab
class TodoListTab(ThreadedTab):
def __init__(self, root: Frame | Tk, **kwargs):
self.task = StringVar()
self.tasks = []
super().__init__(root, **kwargs)
def build(self):
# Create the main frame for the TODO list interface
self.todo_frame = Frame(self)
self.todo_frame.pack(fill="both", expand=True)
# Entry field for adding a task
self.task_entry = Entry(self.todo_frame, textvariable=self.task)
self.task_entry.pack(fill="x", padx=5, pady=5)
# Button to add a task
self.add_task_button = Button(self.todo_frame, text="Add Task", command=self.add_task)
self.add_task_button.pack(padx=5, pady=5)
# Listbox to display tasks
self.task_listbox = Listbox(self.todo_frame)
self.task_listbox.pack(fill="both", expand=True, padx=5, pady=5)
# Button to delete a selected task
self.delete_task_button = Button(self.todo_frame, text="Delete Selected", command=self.delete_task)
self.delete_task_button.pack(padx=5, pady=5)
def add_task(self):
task_text = self.task.get()
if task_text:
self.tasks.append(task_text)
self.update_task_listbox()
self.task.set("") # Clear the entry field
else:
messagebox.showwarning("Warning", "Task cannot be empty!")
def delete_task(self):
selected_indices = self.task_listbox.curselection()
if selected_indices:
for index in selected_indices[::-1]: # Reverse to avoid index shifting issues
del self.tasks[index]
self.update_task_listbox()
else:
messagebox.showwarning("Warning", "No task selected to delete!")
def update_task_listbox(self):
self.task_listbox.delete(0, "end")
for task in self.tasks:
self.task_listbox.insert("end", task)
def task(self):
# Placeholder for threaded behavior if needed in the future
time.sleep(1)

62
app/widgets/TodoTab.py Normal file
View File

@ -0,0 +1,62 @@
import os
from tkinter import Frame, Entry, Button, Listbox, END, StringVar, Tk
from app.widgets.abc import ThreadedTab
class TodoTab(ThreadedTab):
def __init__(self, root: Frame | Tk, **kwargs):
self.todo_file = 'todo.list'
self.todo_items = []
self.load_todo_list()
super().__init__(root, **kwargs)
def build(self):
self.todo_frame = Frame(self)
self.todo_frame.pack(fill="both", expand=True)
self.todo_listbox = Listbox(self.todo_frame)
self.todo_listbox.pack(fill="both", expand=True, padx=5, pady=5)
self.todo_listbox.bind("<Double-Button-1>", self.remove_todo)
self.new_todo_var = StringVar()
self.new_todo_entry = Entry(self.todo_frame, textvariable=self.new_todo_var)
self.new_todo_entry.pack(fill="x", padx=5, pady=5)
self.new_todo_entry.bind("<Return>", self.add_todo)
self.add_button = Button(self.todo_frame, text="Add", command=self.add_todo)
self.add_button.pack(padx=5, pady=5)
self.update_listbox()
def load_todo_list(self):
if os.path.exists(self.todo_file):
with open(self.todo_file, 'r') as file:
self.todo_items = [line.strip() for line in file.readlines()]
def save_todo_list(self):
with open(self.todo_file, 'w') as file:
for item in self.todo_items:
file.write(f"{item}\n")
def add_todo(self, event=None):
new_todo = self.new_todo_var.get().strip()
if new_todo:
self.todo_items.append(new_todo)
self.new_todo_var.set("")
self.update_listbox()
self.save_todo_list()
def remove_todo(self, event=None):
selected_indices = self.todo_listbox.curselection()
for index in selected_indices[::-1]:
del self.todo_items[index]
self.update_listbox()
self.save_todo_list()
def update_listbox(self):
self.todo_listbox.delete(0, END)
for item in self.todo_items:
self.todo_listbox.insert(END, item)
def task(self, *args):
pass

View File

@ -3,21 +3,20 @@ import psutil
from .abc import ThreadedLabel
class CPULabel(ThreadedLabel):
def task(self, *args):
cpu_percent = psutil.cpu_percent()
try: self.config(text=f'CPU: {cpu_percent}%')
except RuntimeError: pass
time.sleep(1)
self.config(text=f'CPU: {cpu_percent}%')
class RAMLabel(ThreadedLabel):
def task(self, *args):
memory = psutil.virtual_memory()
try: self.config(text=f'RAM: {memory.percent}%')
except RuntimeError: pass
time.sleep(1)
self.config(text=f'RAM: {memory.percent}%')
class BatteryLabel(ThreadedLabel):
@ -35,16 +34,17 @@ class BatteryLabel(ThreadedLabel):
else:
text += f', ({time_left // 3600}h {time_left % 3600 // 60}m left)'
try: self.config(text=text)
except RuntimeError: pass # Catch update on closed widget
try:
self.config(text=text)
except RuntimeError:
pass # Catch update on closed widget
time.sleep(1)
class NetworkLabel(ThreadedLabel):
def task(self, *args):
network = psutil.net_io_counters()
try: self.config(text=f'Net: {network.bytes_sent / 1024 / 1024:.2f} MB snt,'
self.config(text=f'Net: {network.bytes_sent / 1024 / 1024:.2f} MB snt,'
f' {network.bytes_recv / 1024 / 1024:.2f} MB rcv')
except RuntimeError: pass # Catch update on closed widget
time.sleep(1)

125
app/widgets/WeatherTab.py Normal file
View File

@ -0,0 +1,125 @@
import threading
import requests
from bs4 import BeautifulSoup
from tkinter import Frame, Label, PhotoImage, Tk
from app.widgets.abc import ThreadedTab
import cairosvg
from io import BytesIO
class WeatherTab(ThreadedTab):
def __init__(self, root: Frame | Tk, city: str, **kwargs):
self.city = city
self.weather_info = {}
self.weather_image = None
self.weather_frame = None
self.weather_label = None
self.weather_image_label = None
self.city_label = None
self.real_feel_label = None
self.wind_label = None
self.wind_gusts_label = None
self.air_quality_label = None
super().__init__(root, **kwargs)
def build(self):
self.weather_frame = Frame(self)
self.weather_frame.pack(fill="both", expand=True)
self.city_label = Label(self.weather_frame, text=f"Weather in {self.city}", font=("Helvetica", 16))
self.city_label.grid(row=0, column=0, columnspan=2, pady=10, sticky="ew")
self.weather_image_label = Label(self.weather_frame)
self.weather_image_label.grid(row=1, column=0, padx=10, sticky="ew")
self.weather_label = Label(self.weather_frame, text="", font=("Helvetica", 14))
self.weather_label.grid(row=1, column=1, padx=10, sticky="ew")
self.real_feel_label = Label(self.weather_frame, text="", font=("Helvetica", 12))
self.real_feel_label.grid(row=2, column=1, padx=10, sticky="ew")
self.wind_label = Label(self.weather_frame, text="", font=("Helvetica", 12))
self.wind_label.grid(row=3, column=1, padx=10, sticky="ew")
self.wind_gusts_label = Label(self.weather_frame, text="", font=("Helvetica", 12))
self.wind_gusts_label.grid(row=4, column=1, padx=10, sticky="ew")
self.air_quality_label = Label(self.weather_frame, text="", font=("Helvetica", 12))
self.air_quality_label.grid(row=5, column=1, padx=10, sticky="ew")
self.weather_frame.columnconfigure(0, weight=1)
self.weather_frame.columnconfigure(1, weight=1)
# Ensure the frame fills the entire parent space
self.grid(row=0, column=0, sticky="nsew")
self.master.rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
def task(self, *args):
self.fetch_weather_data()
self.update_ui()
def fetch_weather_data(self):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
}
search_url = f"https://www.accuweather.com/en/search-locations?query={self.city}"
search_response = requests.get(search_url, headers=headers)
search_soup = BeautifulSoup(search_response.text, 'html.parser')
location_list = search_soup.find('div', class_='locations-list content-module')
if not location_list:
print("Location list not found")
return
location_link = location_list.find('a')['href']
weather_url = f"https://www.accuweather.com{location_link}"
weather_response = requests.get(weather_url, headers=headers)
weather_soup = BeautifulSoup(weather_response.text, 'html.parser')
weather_icon_path = weather_soup.find('svg', class_='weather-icon')['data-src']
weather_icon_url = f"https://www.accuweather.com{weather_icon_path}"
weather_icon_response = requests.get(weather_icon_url, headers=headers)
weather_icon_svg = weather_icon_response.content
# Convert SVG to PNG and resize to 50x50
weather_icon_png = cairosvg.svg2png(bytestring=weather_icon_svg, output_width=50, output_height=50)
weather_icon_image = PhotoImage(data=BytesIO(weather_icon_png).getvalue())
temperature = weather_soup.find('div', class_='temp').text
real_feel = weather_soup.find('div', class_='real-feel').text.strip().replace('RealFeel®', '').strip()
details_container = weather_soup.find('div', class_='details-container')
wind = details_container.find('span', text='Wind').find_next('span', class_='value').text.strip() if details_container else 'N/A'
wind_gusts = details_container.find('span', text='Wind Gusts').find_next('span', class_='value').text.strip() if details_container else 'N/A'
air_quality = details_container.find('span', text='Air Quality').find_next('span', class_='value').text.strip() if details_container else 'N/A'
self.weather_info = {
'icon': weather_icon_image,
'temperature': temperature,
'real_feel': real_feel,
'wind': wind,
'wind_gusts': wind_gusts,
'air_quality': air_quality
}
except Exception as e:
print(f"Error fetching weather data: {e}")
def update_ui(self):
if self.weather_info:
weather_text = f"{self.weather_info['temperature']}"
self.weather_label.config(text=weather_text)
self.weather_image_label.config(image=self.weather_info['icon'])
self.weather_image_label.image = self.weather_info['icon']
self.real_feel_label.config(text=f"RealFeel: {self.weather_info['real_feel']}")
self.wind_label.config(text=f"Wind: {self.weather_info['wind']}")
self.wind_gusts_label.config(text=f"Wind Gusts: {self.weather_info['wind_gusts']}")
self.air_quality_label.config(text=f"Air Quality: {self.weather_info['air_quality']}")
def changeCity(self, city):
self.city = city
self.city_label.config(text=f"Weather in {self.city}")
threading.Thread(target=self.task).start()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,5 @@
import threading
import time
from abc import ABC, abstractmethod
from threading import Thread
from tkinter import Label
@ -6,17 +7,22 @@ from tkinter import Label
class ThreadedLabel(ABC, Label):
def __init__(self, root: Label, stop_event: threading.Event, **kwargs):
def __init__(self, root: Label, stop_event: threading.Event, refresh_rate = None, **kwargs):
super().__init__(root, **kwargs)
self.stop_event = stop_event
self.refresh_rate = refresh_rate
self.declared_thread: Thread = threading.Thread(target=self.__loop)
self.declared_thread.start()
def __loop(self):
while not self.stop_event.is_set():
self.task()
start = time.time()
while time.time() - start < self.refresh_rate:
if self.stop_event.is_set():
break
time.sleep(0.2)
@abstractmethod
def task(self, *args):
pass
pass

View File

@ -1,4 +1,5 @@
import threading
import time
from abc import ABC, abstractmethod
from tkinter import Tk
from tkinter.ttk import Notebook
@ -6,9 +7,10 @@ from tkinter import Frame
class ThreadedTab(ABC, Frame):
def __init__(self, root: Notebook | Tk, stop_event: threading.Event, **kwargs):
def __init__(self, root: Notebook | Tk, stop_event: threading.Event, refresh_rate=1, **kwargs):
super().__init__(root, **kwargs)
self.stop_event = stop_event
self.refresh_rate = refresh_rate
self._thread = threading.Thread(target=self.__loop)
self.build()
self._thread.start()
@ -16,6 +18,11 @@ class ThreadedTab(ABC, Frame):
def __loop(self):
while not self.stop_event.is_set():
self.task()
start = time.time()
while time.time() - start < self.refresh_rate:
if self.stop_event.is_set():
break
time.sleep(0.2)
@abstractmethod
def build(self):
@ -23,4 +30,4 @@ class ThreadedTab(ABC, Frame):
@abstractmethod
def task(self, *args):
raise NotImplementedError("Method not implemented")
raise NotImplementedError("Method not implemented")