diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index e318c95..94a25f7 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,8 +1,6 @@
-
-
\ No newline at end of file
diff --git a/app/ConfigMgr.py b/app/ConfigMgr.py
index 9d2a8ce..5999c32 100644
--- a/app/ConfigMgr.py
+++ b/app/ConfigMgr.py
@@ -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)
diff --git a/app/__pycache__/ConfigMgr.cpython-312.pyc b/app/__pycache__/ConfigMgr.cpython-312.pyc
index 409d567..3fd4046 100644
Binary files a/app/__pycache__/ConfigMgr.cpython-312.pyc and b/app/__pycache__/ConfigMgr.cpython-312.pyc differ
diff --git a/app/__pycache__/ConfigMgr.cpython-313.pyc b/app/__pycache__/ConfigMgr.cpython-313.pyc
index 3082899..e282995 100644
Binary files a/app/__pycache__/ConfigMgr.cpython-313.pyc and b/app/__pycache__/ConfigMgr.cpython-313.pyc differ
diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc
index 977055b..8c9fe6f 100644
Binary files a/app/__pycache__/__init__.cpython-313.pyc and b/app/__pycache__/__init__.cpython-313.pyc differ
diff --git a/app/chat_server/Server.py b/app/chat_server/Server.py
index d4dfb45..1c9a674 100644
--- a/app/chat_server/Server.py
+++ b/app/chat_server/Server.py
@@ -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()
diff --git a/app/chat_server/__pycache__/Server.cpython-312.pyc b/app/chat_server/__pycache__/Server.cpython-312.pyc
index b6f4dc0..a12ce92 100644
Binary files a/app/chat_server/__pycache__/Server.cpython-312.pyc and b/app/chat_server/__pycache__/Server.cpython-312.pyc differ
diff --git a/app/chat_server/__pycache__/Server.cpython-313.pyc b/app/chat_server/__pycache__/Server.cpython-313.pyc
index 1c98264..0f7935a 100644
Binary files a/app/chat_server/__pycache__/Server.cpython-313.pyc and b/app/chat_server/__pycache__/Server.cpython-313.pyc differ
diff --git a/app/chat_server/__pycache__/__init__.cpython-313.pyc b/app/chat_server/__pycache__/__init__.cpython-313.pyc
index 76fdae9..20e5ff2 100644
Binary files a/app/chat_server/__pycache__/__init__.cpython-313.pyc and b/app/chat_server/__pycache__/__init__.cpython-313.pyc differ
diff --git a/app/chat_server/messages.txt b/app/chat_server/messages.txt
index b463e24..a3d7628 100644
--- a/app/chat_server/messages.txt
+++ b/app/chat_server/messages.txt
@@ -1,2 +1,2 @@
-1|Santi|Hola como estas?
-2|Santi|Andres no me copies el TO DO
+1|User|Hola
+2|feo|hola
diff --git a/app/config.ini b/app/config.ini
index f0b8909..6f3c16c 100644
--- a/app/config.ini
+++ b/app/config.ini
@@ -1,4 +1,7 @@
[Chat]
server = http://localhost:2020
-name = Santi
+name = feo
+
+[Weather]
+city = Denia
diff --git a/app/main.py b/app/main.py
index bc80d0f..b22d801 100644
--- a/app/main.py
+++ b/app/main.py
@@ -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()
\ No newline at end of file
diff --git a/app/music/Alan Walker, Dash Berlin & Vikkstar - Better Off (Alone, Pt. III) - Official Music Video.mp3 b/app/music/Alan Walker, Dash Berlin & Vikkstar - Better Off (Alone, Pt. III) - Official Music Video.mp3
new file mode 100644
index 0000000..1b30f77
Binary files /dev/null and b/app/music/Alan Walker, Dash Berlin & Vikkstar - Better Off (Alone, Pt. III) - Official Music Video.mp3 differ
diff --git a/app/music/Artemas - i like the way you kiss me (official music video).mp3 b/app/music/Artemas - i like the way you kiss me (official music video).mp3
new file mode 100644
index 0000000..25026ef
Binary files /dev/null and b/app/music/Artemas - i like the way you kiss me (official music video).mp3 differ
diff --git a/app/music/Dimitri Vegas & Like Mike & Tiësto & Dido & W&W - Thank You (Not So Bad) (Official video).mp3 b/app/music/Dimitri Vegas & Like Mike & Tiësto & Dido & W&W - Thank You (Not So Bad) (Official video).mp3
new file mode 100644
index 0000000..970ee00
Binary files /dev/null and b/app/music/Dimitri Vegas & Like Mike & Tiësto & Dido & W&W - Thank You (Not So Bad) (Official video).mp3 differ
diff --git a/app/music/Emei - Irresponsible (Official Visualizer).mp3 b/app/music/Emei - Irresponsible (Official Visualizer).mp3
new file mode 100644
index 0000000..5afdce2
Binary files /dev/null and b/app/music/Emei - Irresponsible (Official Visualizer).mp3 differ
diff --git a/app/music/Kygo, Ava Max - Whatever (Official Video).mp3 b/app/music/Kygo, Ava Max - Whatever (Official Video).mp3
new file mode 100644
index 0000000..76a9343
Binary files /dev/null and b/app/music/Kygo, Ava Max - Whatever (Official Video).mp3 differ
diff --git a/app/requirements.txt b/app/requirements.txt
index 3856835..7464465 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -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
diff --git a/app/todo.list b/app/todo.list
new file mode 100644
index 0000000..e69de29
diff --git a/app/widgets/ChatTab.py b/app/widgets/ChatTab.py
index 925854d..de1a65a 100644
--- a/app/widgets/ChatTab.py
+++ b/app/widgets/ChatTab.py
@@ -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
diff --git a/app/widgets/ClockLabel.py b/app/widgets/ClockLabel.py
index c4f784c..f5957cb 100644
--- a/app/widgets/ClockLabel.py
+++ b/app/widgets/ClockLabel.py
@@ -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)
\ No newline at end of file
+ self.config(text=label_text)
\ No newline at end of file
diff --git a/app/widgets/MusicDownloadTab.py b/app/widgets/MusicDownloadTab.py
new file mode 100644
index 0000000..098a1b9
--- /dev/null
+++ b/app/widgets/MusicDownloadTab.py
@@ -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)
\ No newline at end of file
diff --git a/app/widgets/MusicPlayerTab.py b/app/widgets/MusicPlayerTab.py
new file mode 100644
index 0000000..3b1a5c8
--- /dev/null
+++ b/app/widgets/MusicPlayerTab.py
@@ -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()
diff --git a/app/widgets/TodoListTab.py b/app/widgets/TodoListTab.py
new file mode 100644
index 0000000..e96893c
--- /dev/null
+++ b/app/widgets/TodoListTab.py
@@ -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)
diff --git a/app/widgets/TodoTab.py b/app/widgets/TodoTab.py
new file mode 100644
index 0000000..d177c2c
--- /dev/null
+++ b/app/widgets/TodoTab.py
@@ -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("", 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("", 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
\ No newline at end of file
diff --git a/app/widgets/UsageLabels.py b/app/widgets/UsageLabels.py
index 8e7e709..6886048 100644
--- a/app/widgets/UsageLabels.py
+++ b/app/widgets/UsageLabels.py
@@ -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)
\ No newline at end of file
diff --git a/app/widgets/WeatherTab.py b/app/widgets/WeatherTab.py
new file mode 100644
index 0000000..d266598
--- /dev/null
+++ b/app/widgets/WeatherTab.py
@@ -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()
\ No newline at end of file
diff --git a/app/widgets/__pycache__/ChatTab.cpython-312.pyc b/app/widgets/__pycache__/ChatTab.cpython-312.pyc
index a2bf680..f8885cf 100644
Binary files a/app/widgets/__pycache__/ChatTab.cpython-312.pyc and b/app/widgets/__pycache__/ChatTab.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/ChatTab.cpython-313.pyc b/app/widgets/__pycache__/ChatTab.cpython-313.pyc
index 5f34974..0f6ada6 100644
Binary files a/app/widgets/__pycache__/ChatTab.cpython-313.pyc and b/app/widgets/__pycache__/ChatTab.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/ClockLabel.cpython-312.pyc b/app/widgets/__pycache__/ClockLabel.cpython-312.pyc
index 2471248..7fc86d9 100644
Binary files a/app/widgets/__pycache__/ClockLabel.cpython-312.pyc and b/app/widgets/__pycache__/ClockLabel.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/ClockLabel.cpython-313.pyc b/app/widgets/__pycache__/ClockLabel.cpython-313.pyc
index 82abdbb..599e940 100644
Binary files a/app/widgets/__pycache__/ClockLabel.cpython-313.pyc and b/app/widgets/__pycache__/ClockLabel.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/MusicDownloadTab.cpython-312.pyc b/app/widgets/__pycache__/MusicDownloadTab.cpython-312.pyc
new file mode 100644
index 0000000..83bf077
Binary files /dev/null and b/app/widgets/__pycache__/MusicDownloadTab.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/MusicDownloadTab.cpython-313.pyc b/app/widgets/__pycache__/MusicDownloadTab.cpython-313.pyc
new file mode 100644
index 0000000..9f8ea8a
Binary files /dev/null and b/app/widgets/__pycache__/MusicDownloadTab.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/MusicPlayerTab.cpython-312.pyc b/app/widgets/__pycache__/MusicPlayerTab.cpython-312.pyc
new file mode 100644
index 0000000..abc9029
Binary files /dev/null and b/app/widgets/__pycache__/MusicPlayerTab.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/MusicPlayerTab.cpython-313.pyc b/app/widgets/__pycache__/MusicPlayerTab.cpython-313.pyc
new file mode 100644
index 0000000..c1acc1d
Binary files /dev/null and b/app/widgets/__pycache__/MusicPlayerTab.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/TodoTab.cpython-312.pyc b/app/widgets/__pycache__/TodoTab.cpython-312.pyc
new file mode 100644
index 0000000..e20ce7a
Binary files /dev/null and b/app/widgets/__pycache__/TodoTab.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/TodoTab.cpython-313.pyc b/app/widgets/__pycache__/TodoTab.cpython-313.pyc
new file mode 100644
index 0000000..3313ee3
Binary files /dev/null and b/app/widgets/__pycache__/TodoTab.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/UsageLabels.cpython-312.pyc b/app/widgets/__pycache__/UsageLabels.cpython-312.pyc
index 1281f7e..c603cdb 100644
Binary files a/app/widgets/__pycache__/UsageLabels.cpython-312.pyc and b/app/widgets/__pycache__/UsageLabels.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/UsageLabels.cpython-313.pyc b/app/widgets/__pycache__/UsageLabels.cpython-313.pyc
index 974e62f..9f9f7a1 100644
Binary files a/app/widgets/__pycache__/UsageLabels.cpython-313.pyc and b/app/widgets/__pycache__/UsageLabels.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/WeatherTab.cpython-312.pyc b/app/widgets/__pycache__/WeatherTab.cpython-312.pyc
new file mode 100644
index 0000000..3390539
Binary files /dev/null and b/app/widgets/__pycache__/WeatherTab.cpython-312.pyc differ
diff --git a/app/widgets/__pycache__/WeatherTab.cpython-313.pyc b/app/widgets/__pycache__/WeatherTab.cpython-313.pyc
new file mode 100644
index 0000000..5a3ea41
Binary files /dev/null and b/app/widgets/__pycache__/WeatherTab.cpython-313.pyc differ
diff --git a/app/widgets/__pycache__/__init__.cpython-313.pyc b/app/widgets/__pycache__/__init__.cpython-313.pyc
index 2b53f3e..3e32371 100644
Binary files a/app/widgets/__pycache__/__init__.cpython-313.pyc and b/app/widgets/__pycache__/__init__.cpython-313.pyc differ
diff --git a/app/widgets/abc/ThreadedLabel.py b/app/widgets/abc/ThreadedLabel.py
index 4e37887..cebbcac 100644
--- a/app/widgets/abc/ThreadedLabel.py
+++ b/app/widgets/abc/ThreadedLabel.py
@@ -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
\ No newline at end of file
+ pass
diff --git a/app/widgets/abc/ThreadedTab.py b/app/widgets/abc/ThreadedTab.py
index 0d90e5e..254ed6c 100644
--- a/app/widgets/abc/ThreadedTab.py
+++ b/app/widgets/abc/ThreadedTab.py
@@ -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")
\ No newline at end of file
+ raise NotImplementedError("Method not implemented")
diff --git a/app/widgets/abc/__pycache__/ThreadedLabel.cpython-312.pyc b/app/widgets/abc/__pycache__/ThreadedLabel.cpython-312.pyc
index 9b99c2e..7204796 100644
Binary files a/app/widgets/abc/__pycache__/ThreadedLabel.cpython-312.pyc and b/app/widgets/abc/__pycache__/ThreadedLabel.cpython-312.pyc differ
diff --git a/app/widgets/abc/__pycache__/ThreadedLabel.cpython-313.pyc b/app/widgets/abc/__pycache__/ThreadedLabel.cpython-313.pyc
index 41aebf3..bba0f3c 100644
Binary files a/app/widgets/abc/__pycache__/ThreadedLabel.cpython-313.pyc and b/app/widgets/abc/__pycache__/ThreadedLabel.cpython-313.pyc differ
diff --git a/app/widgets/abc/__pycache__/ThreadedTab.cpython-312.pyc b/app/widgets/abc/__pycache__/ThreadedTab.cpython-312.pyc
index ae9a589..d891ec0 100644
Binary files a/app/widgets/abc/__pycache__/ThreadedTab.cpython-312.pyc and b/app/widgets/abc/__pycache__/ThreadedTab.cpython-312.pyc differ
diff --git a/app/widgets/abc/__pycache__/ThreadedTab.cpython-313.pyc b/app/widgets/abc/__pycache__/ThreadedTab.cpython-313.pyc
index beb64f2..9bf6c48 100644
Binary files a/app/widgets/abc/__pycache__/ThreadedTab.cpython-313.pyc and b/app/widgets/abc/__pycache__/ThreadedTab.cpython-313.pyc differ
diff --git a/app/widgets/abc/__pycache__/__init__.cpython-313.pyc b/app/widgets/abc/__pycache__/__init__.cpython-313.pyc
index 9bfc320..8fd9c27 100644
Binary files a/app/widgets/abc/__pycache__/__init__.cpython-313.pyc and b/app/widgets/abc/__pycache__/__init__.cpython-313.pyc differ