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"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app" vcs="Git" />
</component> </component>
</project> </project>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,28 @@
beautifulsoup4==4.12.3
blinker==1.9.0 blinker==1.9.0
cairocffi==1.7.1
CairoSVG==2.7.1
certifi==2024.8.30 certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.4.0 charset-normalizer==3.4.0
click==8.1.7 click==8.1.7
cssselect2==0.7.0
defusedxml==0.7.1
Flask==3.1.0 Flask==3.1.0
geographiclib==2.0
geopy==2.4.1
idna==3.10 idna==3.10
itsdangerous==2.2.0 itsdangerous==2.2.0
Jinja2==3.1.4 Jinja2==3.1.4
MarkupSafe==3.0.2 MarkupSafe==3.0.2
psutil==6.1.0 psutil==6.1.0
pycparser==2.22
pygame==2.6.1
requests==2.32.3 requests==2.32.3
soupsieve==2.6
tinycss2==1.4.0
urllib3==2.2.3 urllib3==2.2.3
weatherkit==1.1.1
webencodings==0.5.1
Werkzeug==3.1.3 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 self.conn = True
except requests.ConnectionError: except requests.ConnectionError:
self.disconnected() self.disconnected()
time.sleep(1)
def build(self): def build(self):
# Create the main frame for the chat interface # Create the main frame for the chat interface

View File

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

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 from .abc import ThreadedLabel
class CPULabel(ThreadedLabel): class CPULabel(ThreadedLabel):
def task(self, *args): def task(self, *args):
cpu_percent = psutil.cpu_percent() cpu_percent = psutil.cpu_percent()
try: self.config(text=f'CPU: {cpu_percent}%') self.config(text=f'CPU: {cpu_percent}%')
except RuntimeError: pass
time.sleep(1)
class RAMLabel(ThreadedLabel): class RAMLabel(ThreadedLabel):
def task(self, *args): def task(self, *args):
memory = psutil.virtual_memory() memory = psutil.virtual_memory()
try: self.config(text=f'RAM: {memory.percent}%') self.config(text=f'RAM: {memory.percent}%')
except RuntimeError: pass
time.sleep(1)
class BatteryLabel(ThreadedLabel): class BatteryLabel(ThreadedLabel):
@ -35,16 +34,17 @@ class BatteryLabel(ThreadedLabel):
else: else:
text += f', ({time_left // 3600}h {time_left % 3600 // 60}m left)' text += f', ({time_left // 3600}h {time_left % 3600 // 60}m left)'
try: self.config(text=text) try:
except RuntimeError: pass # Catch update on closed widget self.config(text=text)
except RuntimeError:
pass # Catch update on closed widget
time.sleep(1) time.sleep(1)
class NetworkLabel(ThreadedLabel): class NetworkLabel(ThreadedLabel):
def task(self, *args): def task(self, *args):
network = psutil.net_io_counters() 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') 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 threading
import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from threading import Thread from threading import Thread
from tkinter import Label from tkinter import Label
@ -6,17 +7,22 @@ from tkinter import Label
class ThreadedLabel(ABC, 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) super().__init__(root, **kwargs)
self.stop_event = stop_event self.stop_event = stop_event
self.refresh_rate = refresh_rate
self.declared_thread: Thread = threading.Thread(target=self.__loop) self.declared_thread: Thread = threading.Thread(target=self.__loop)
self.declared_thread.start() self.declared_thread.start()
def __loop(self): def __loop(self):
while not self.stop_event.is_set(): while not self.stop_event.is_set():
self.task() self.task()
start = time.time()
while time.time() - start < self.refresh_rate:
if self.stop_event.is_set():
break
time.sleep(0.2)
@abstractmethod @abstractmethod
def task(self, *args): def task(self, *args):
pass pass

View File

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