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