From 5acc9c8b368c57bda555e7f88355e7d3fff84f26 Mon Sep 17 00:00:00 2001 From: freeram Date: Thu, 2 May 2024 23:18:27 -0600 Subject: [PATCH 1/9] Add semi-working RPC toggle button --- src/app.py | 2 +- src/frames/pomodoro_frame.py | 41 ++++++++++++++++++++++++++++-------- src/logic/richpresence.py | 22 +++++++++++++++++-- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/app.py b/src/app.py index 7b2c80e..e7df8e0 100644 --- a/src/app.py +++ b/src/app.py @@ -32,7 +32,7 @@ def on_tab_change(self): class PomodoroApp(ctk.CTk): WIDTH = 350 - HEIGHT = 400 + HEIGHT = 450 def __init__(self): super().__init__() diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index 7db699d..35797b9 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -1,6 +1,7 @@ import time import threading import customtkinter as ctk +from CTkMessagebox import CTkMessagebox from datetime import datetime, timedelta from src.utils import load_config, load_data, save_data, beep, DEF_POMODORO_MINS, DEF_SB_MINS, DEF_LB_MINS, DEF_SB_BEFORE_L from src.logic.richpresence import RichPresence @@ -9,6 +10,8 @@ BREAK_HOVER = "#adaaaa" RESET_BTN_COLOR = "#cca508" RESET_HOVER = "#e3b707" +CONNECTED_TEXT = "#45b54e" +DISCONNECTED_TEXT = "#f75a4f" class PomodoroFrame(ctk.CTkFrame): @@ -44,7 +47,11 @@ def initialize_ui(self, config): self.lb_button.pack(pady=(0, 10)) self.reset_button = ctk.CTkButton(self, text="Reset", font=("Roboto", 17), fg_color=RESET_BTN_COLOR, hover_color=RESET_HOVER, command=self.reset) - self.reset_button.pack() + self.reset_button.pack(pady=(0, 10)) + + self.discord_button = ctk.CTkButton(self, text="Connected to Discord [click to disconnect]", font=("Roboto", 12), + fg_color="transparent", text_color=CONNECTED_TEXT, width=70, command=self.toggle_rpc) + self.discord_button.pack(pady=(20, 0)) def initialize_state(self, config): self.running = False @@ -73,16 +80,32 @@ def initialize_rpc(self): self.rpc_thread = threading.Thread(target=self.update_rpc, daemon=True) self.rpc_thread.start() + def toggle_rpc(self): + if not self.rpc.connected: + if self.rpc.connect(): + self.discord_button.configure(text="Connected to Discord [click to disconnect]", text_color=CONNECTED_TEXT) + else: + CTkMessagebox(title="Error", message="Reconnecting to Discord failed\nCheck console for error output", icon="cancel") + else: + if self.rpc.disconnect(): + self.discord_button.configure(text="Not connected to Discord [click to connect]", text_color=DISCONNECTED_TEXT) + else: + CTkMessagebox(title="Error", message="Reconnecting to Discord failed\nCheck console for error output", icon="cancel") + def update_rpc(self): while True: - if self.break_running: - self.rpc.break_state(self.seconds_studied, self.start_time_timestamp, self.end_time_timestamp) - elif self.running: - self.rpc.running_state(self.session_counter + 1, self.start_time_timestamp, self.end_time_timestamp) - elif self.paused: - self.rpc.paused_state(self.start_time_timestamp) - else: - self.rpc.idling_state() + if self.rpc.connected: + try: + if self.break_running: + self.rpc.break_state(self.seconds_studied, self.start_time_timestamp, self.end_time_timestamp) + elif self.running: + self.rpc.running_state(self.session_counter + 1, self.start_time_timestamp, self.end_time_timestamp) + elif self.paused: + self.rpc.paused_state(self.start_time_timestamp) + else: + self.rpc.idling_state() + except Exception as e: + print(f"Error updating Rich Presence: {e}") # Discord-imposed rate limit time.sleep(15) diff --git a/src/logic/richpresence.py b/src/logic/richpresence.py index db1526f..9084ebd 100644 --- a/src/logic/richpresence.py +++ b/src/logic/richpresence.py @@ -9,11 +9,29 @@ def __init__(self): super().__init__(client_id=CLIENT_ID) self.launch_time = datetime.now().timestamp() # Can only update every 15 seconds + self.connected = False + self.connect() + + def connect(self): try: - self.connect() + super().connect() self.idling_state() + self.connected = True + except Exception as e: + print(f"Failed to reconnect to Discord: {e}") + self.connected = False + return self.connected + + def disconnect(self): + if not self.connected: + return True + try: + super().close() + self.connected = False except Exception as e: - print(f"Failed to connect to Discord: {e}") + print(f"Failed to disconnect from Discord: {e}") + self.connected = True + return not self.connected def format_time(self, seconds_studied): total_seconds = seconds_studied From 4ae4dfe2f797b6a582c32db00ce763a740ed0ebb Mon Sep 17 00:00:00 2001 From: freeram Date: Fri, 3 May 2024 04:01:07 -0600 Subject: [PATCH 2/9] De-spaghettify, separate threads for connecting/disconnecting RPC --- src/frames/pomodoro_frame.py | 48 +++++++++++++++++++++--------------- src/logic/richpresence.py | 13 +++++----- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index 35797b9..d06758e 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -50,7 +50,7 @@ def initialize_ui(self, config): self.reset_button.pack(pady=(0, 10)) self.discord_button = ctk.CTkButton(self, text="Connected to Discord [click to disconnect]", font=("Roboto", 12), - fg_color="transparent", text_color=CONNECTED_TEXT, width=70, command=self.toggle_rpc) + fg_color="transparent", text_color=CONNECTED_TEXT, hover=False, width=70, command=self.toggle_rpc) self.discord_button.pack(pady=(20, 0)) def initialize_state(self, config): @@ -81,31 +81,39 @@ def initialize_rpc(self): self.rpc_thread.start() def toggle_rpc(self): + self.discord_button.configure(state="disabled") if not self.rpc.connected: - if self.rpc.connect(): - self.discord_button.configure(text="Connected to Discord [click to disconnect]", text_color=CONNECTED_TEXT) - else: - CTkMessagebox(title="Error", message="Reconnecting to Discord failed\nCheck console for error output", icon="cancel") + self.discord_button.configure(text="Connecting...") + threading.Thread(target=self.connect_rpc, daemon=True).start() else: - if self.rpc.disconnect(): - self.discord_button.configure(text="Not connected to Discord [click to connect]", text_color=DISCONNECTED_TEXT) - else: - CTkMessagebox(title="Error", message="Reconnecting to Discord failed\nCheck console for error output", icon="cancel") + self.discord_button.configure(text="Disconnecting...") + threading.Thread(target=self.disconnect_rpc, daemon=True).start() + + def connect_rpc(self): + if self.rpc.connect(): + self.discord_button.configure(text="Connected to Discord [click to disconnect]", text_color=CONNECTED_TEXT, state="normal") + else: + self.discord_button.configure(text="Not connected to Discord [click to connect]", text_color=DISCONNECTED_TEXT, state="normal") + CTkMessagebox(title="Error", message="Connecting to Discord failed\nCheck console for error output", icon="cancel") + + def disconnect_rpc(self): + if self.rpc.disconnect(): + self.discord_button.configure(text="Not connected to Discord [click to connect]", text_color=DISCONNECTED_TEXT, state="normal") + else: + self.discord_button.configure(text="Connected to Discord [click to disconnect]", text_color=CONNECTED_TEXT, state="normal") + CTkMessagebox(title="Error", message="Disconnecting from Discord failed\nCheck console for error output", icon="cancel") def update_rpc(self): while True: if self.rpc.connected: - try: - if self.break_running: - self.rpc.break_state(self.seconds_studied, self.start_time_timestamp, self.end_time_timestamp) - elif self.running: - self.rpc.running_state(self.session_counter + 1, self.start_time_timestamp, self.end_time_timestamp) - elif self.paused: - self.rpc.paused_state(self.start_time_timestamp) - else: - self.rpc.idling_state() - except Exception as e: - print(f"Error updating Rich Presence: {e}") + if self.break_running: + self.rpc.break_state(self.seconds_studied, self.start_time_timestamp, self.end_time_timestamp) + elif self.running: + self.rpc.running_state(self.session_counter + 1, self.start_time_timestamp, self.end_time_timestamp) + elif self.paused: + self.rpc.paused_state(self.start_time_timestamp) + else: + self.rpc.idling_state() # Discord-imposed rate limit time.sleep(15) diff --git a/src/logic/richpresence.py b/src/logic/richpresence.py index 9084ebd..a40ada9 100644 --- a/src/logic/richpresence.py +++ b/src/logic/richpresence.py @@ -11,32 +11,31 @@ def __init__(self): # Can only update every 15 seconds self.connected = False self.connect() + self.idling_state() def connect(self): try: super().connect() - self.idling_state() self.connected = True + return True except Exception as e: - print(f"Failed to reconnect to Discord: {e}") + print(f"Failed to connect to Discord: {e}") self.connected = False - return self.connected + return False def disconnect(self): - if not self.connected: - return True try: super().close() self.connected = False + return True except Exception as e: print(f"Failed to disconnect from Discord: {e}") self.connected = True - return not self.connected + return False def format_time(self, seconds_studied): total_seconds = seconds_studied total_hours = seconds_studied / 3600 - if total_hours < 1: return f"{total_seconds // 60} minute{'s' if total_seconds // 60 != 1 else ''}" else: From 6013ab327c0f593338f7bd26b746c53f12d18147 Mon Sep 17 00:00:00 2001 From: freeram Date: Fri, 3 May 2024 04:32:26 -0600 Subject: [PATCH 3/9] Clean --- src/frames/pomodoro_frame.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index d06758e..884cd18 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -10,8 +10,10 @@ BREAK_HOVER = "#adaaaa" RESET_BTN_COLOR = "#cca508" RESET_HOVER = "#e3b707" -CONNECTED_TEXT = "#45b54e" -DISCONNECTED_TEXT = "#f75a4f" +CONNECTED_COLOR = "#45b54e" +DISCONNECTED_COLOR = "#f75a4f" +CONNECTED_TEXT = "Connected to Discord [click to disconnect]" +DISCONNECTED_TEXT = "Not connected to Discord [click to connect]" class PomodoroFrame(ctk.CTkFrame): @@ -49,8 +51,8 @@ def initialize_ui(self, config): self.reset_button = ctk.CTkButton(self, text="Reset", font=("Roboto", 17), fg_color=RESET_BTN_COLOR, hover_color=RESET_HOVER, command=self.reset) self.reset_button.pack(pady=(0, 10)) - self.discord_button = ctk.CTkButton(self, text="Connected to Discord [click to disconnect]", font=("Roboto", 12), - fg_color="transparent", text_color=CONNECTED_TEXT, hover=False, width=70, command=self.toggle_rpc) + self.discord_button = ctk.CTkButton(self, text=CONNECTED_TEXT, font=("Roboto", 12), + fg_color="transparent", text_color=CONNECTED_COLOR, hover=False, width=70, command=self.toggle_rpc) self.discord_button.pack(pady=(20, 0)) def initialize_state(self, config): @@ -91,16 +93,16 @@ def toggle_rpc(self): def connect_rpc(self): if self.rpc.connect(): - self.discord_button.configure(text="Connected to Discord [click to disconnect]", text_color=CONNECTED_TEXT, state="normal") + self.discord_button.configure(text=CONNECTED_TEXT, text_color=CONNECTED_COLOR, state="normal") else: - self.discord_button.configure(text="Not connected to Discord [click to connect]", text_color=DISCONNECTED_TEXT, state="normal") + self.discord_button.configure(text=DISCONNECTED_TEXT, text_color=DISCONNECTED_COLOR, state="normal") CTkMessagebox(title="Error", message="Connecting to Discord failed\nCheck console for error output", icon="cancel") def disconnect_rpc(self): if self.rpc.disconnect(): - self.discord_button.configure(text="Not connected to Discord [click to connect]", text_color=DISCONNECTED_TEXT, state="normal") + self.discord_button.configure(text=DISCONNECTED_TEXT, text_color=DISCONNECTED_COLOR, state="normal") else: - self.discord_button.configure(text="Connected to Discord [click to disconnect]", text_color=CONNECTED_TEXT, state="normal") + self.discord_button.configure(text=CONNECTED_TEXT, text_color=CONNECTED_COLOR, state="normal") CTkMessagebox(title="Error", message="Disconnecting from Discord failed\nCheck console for error output", icon="cancel") def update_rpc(self): From 69970b03e10c722d487d498d60070674ba2b93a0 Mon Sep 17 00:00:00 2001 From: freeram Date: Fri, 3 May 2024 04:32:51 -0600 Subject: [PATCH 4/9] Add _handle_exceptions wrapper --- src/logic/richpresence.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/logic/richpresence.py b/src/logic/richpresence.py index a40ada9..6130483 100644 --- a/src/logic/richpresence.py +++ b/src/logic/richpresence.py @@ -13,6 +13,17 @@ def __init__(self): self.connect() self.idling_state() + def _handle_exceptions(func): + def wrapper(self, *args, **kwargs): + if not self.connected: + return + try: + func(self, *args, **kwargs) + except Exception as e: + print(f"Error updating Rich Presence: {e}") + self.connected = False + return wrapper + def connect(self): try: super().connect() @@ -41,18 +52,22 @@ def format_time(self, seconds_studied): else: return f"{round(total_hours, 1) if total_hours % 1 != 0 else int(total_hours)} hours" + @_handle_exceptions def idling_state(self): self.update(state="Idling", details=None, start=self.launch_time, large_image="graytomato", large_text="github.com/freeram/pomodoro-discord") + @_handle_exceptions def running_state(self, session, start_time, end_time): self.update(state=f"Session {session}", details="Studying", start=start_time, end=end_time, large_image="tomato", large_text="github.com/freeram/pomodoro-discord") + @_handle_exceptions def paused_state(self, start_time): self.update(state="Paused", details=None, start=start_time, large_image="graytomato", large_text="github.com/freeram/pomodoro-discord") + @_handle_exceptions def break_state(self, seconds_studied, start_time, end_time): self.update(state=f"Time studied: {self.format_time(seconds_studied)}", details="On break", start=start_time, end=end_time, large_image="greentomato", large_text="github.com/freeram/pomodoro-discord") From e0a3520d9ef782649016bd4c92f886a55761aca0 Mon Sep 17 00:00:00 2001 From: freeram Date: Fri, 3 May 2024 19:45:31 -0600 Subject: [PATCH 5/9] Update button if rpc disconnected --- src/frames/pomodoro_frame.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index 884cd18..3416c01 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -116,6 +116,9 @@ def update_rpc(self): self.rpc.paused_state(self.start_time_timestamp) else: self.rpc.idling_state() + else: + if self.discord_button.cget("state") == 'normal': + self.discord_button.configure(text=DISCONNECTED_TEXT, text_color=DISCONNECTED_COLOR) # Discord-imposed rate limit time.sleep(15) From 5bff74b55b1fd73465ce0fafa27eabd9dff9f549 Mon Sep 17 00:00:00 2001 From: freeram Date: Sat, 4 May 2024 03:45:55 -0600 Subject: [PATCH 6/9] Show error if user tries to plot with no data --- src/frames/stats_frame.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/frames/stats_frame.py b/src/frames/stats_frame.py index 7a75c92..4f57578 100644 --- a/src/frames/stats_frame.py +++ b/src/frames/stats_frame.py @@ -1,5 +1,6 @@ import customtkinter as ctk from datetime import datetime +from CTkMessagebox import CTkMessagebox from src.reusable.stats_reusable import StatisticFrame, ButtonFrame from src.utils import load_data from src.logic.graphs import graph_pomodoro_sessions, graph_hours_studied @@ -51,8 +52,20 @@ def update_total_pomodoros(self, data): total_pomodoros = data.get('total_pomodoro_sessions', 0) self.total_pomodoros.set_value(f"{total_pomodoros} session{'s' if total_pomodoros != 1 else ''}") + def _ensure_exists(self, param, msg): + data = load_data() + value = data.get(param, 0) + if value == 0: + CTkMessagebox(title="Error", message=msg, icon="cancel") + return False + return True + def show_sessions_graph(self): + if not self._ensure_exists('total_pomodoro_sessions', 'Cannot graph pomodoro sessions: none recorded'): + return graph_pomodoro_sessions(load_data()) def show_hours_graph(self): + if not self._ensure_exists('total_seconds_studied', 'Cannot graph hours studied: none recorded'): + return graph_hours_studied(load_data()) From abe60863978dfca04beb5ce28df7ea34ca36ec3c Mon Sep 17 00:00:00 2001 From: freeram Date: Sat, 4 May 2024 04:39:00 -0600 Subject: [PATCH 7/9] Show total time studied in Rich Presence idle --- src/frames/pomodoro_frame.py | 2 +- src/logic/richpresence.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index 3416c01..8c25fc8 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -115,7 +115,7 @@ def update_rpc(self): elif self.paused: self.rpc.paused_state(self.start_time_timestamp) else: - self.rpc.idling_state() + self.rpc.idling_state(self.seconds_studied) else: if self.discord_button.cget("state") == 'normal': self.discord_button.configure(text=DISCONNECTED_TEXT, text_color=DISCONNECTED_COLOR) diff --git a/src/logic/richpresence.py b/src/logic/richpresence.py index 6130483..7d63834 100644 --- a/src/logic/richpresence.py +++ b/src/logic/richpresence.py @@ -1,5 +1,6 @@ import pypresence from datetime import datetime +from src.utils import load_data CLIENT_ID = '1215345125002059836' @@ -11,6 +12,7 @@ def __init__(self): # Can only update every 15 seconds self.connected = False self.connect() + self.total_seconds_studied = load_data().get('total_seconds_studied', 0) self.idling_state() def _handle_exceptions(func): @@ -53,9 +55,9 @@ def format_time(self, seconds_studied): return f"{round(total_hours, 1) if total_hours % 1 != 0 else int(total_hours)} hours" @_handle_exceptions - def idling_state(self): - self.update(state="Idling", details=None, start=self.launch_time, large_image="graytomato", - large_text="github.com/freeram/pomodoro-discord") + def idling_state(self, seconds_studied=0): + self.update(state=f"Total time studied: {self.format_time(self.total_seconds_studied + seconds_studied)}", + details="Idling", start=self.launch_time, large_image="graytomato", large_text="github.com/freeram/pomodoro-discord") @_handle_exceptions def running_state(self, session, start_time, end_time): From b3b2a04cfbe69dc60ad7f62e421a020c65c4749a Mon Sep 17 00:00:00 2001 From: freeram Date: Sun, 5 May 2024 15:36:09 -0600 Subject: [PATCH 8/9] Disable button when connecting to Discord, add hover --- src/frames/pomodoro_frame.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index 8c25fc8..18b41d4 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -8,12 +8,17 @@ BREAK_BTN_COLOR = "#9a9a9a" BREAK_HOVER = "#adaaaa" + RESET_BTN_COLOR = "#cca508" RESET_HOVER = "#e3b707" -CONNECTED_COLOR = "#45b54e" -DISCONNECTED_COLOR = "#f75a4f" -CONNECTED_TEXT = "Connected to Discord [click to disconnect]" -DISCONNECTED_TEXT = "Not connected to Discord [click to connect]" + +CONNECTED_TEXT = "Connected to Discord" +CONNECTED_COLOR = "gray65" +CONNECTED_HOVER = "gray77" + +DISCONNECTED_TEXT = "Not connected to Discord" +DISCONNECTED_COLOR = "#d93b3b" +DISCONNECTED_HOVER = "#fa5757" class PomodoroFrame(ctk.CTkFrame): @@ -51,9 +56,17 @@ def initialize_ui(self, config): self.reset_button = ctk.CTkButton(self, text="Reset", font=("Roboto", 17), fg_color=RESET_BTN_COLOR, hover_color=RESET_HOVER, command=self.reset) self.reset_button.pack(pady=(0, 10)) - self.discord_button = ctk.CTkButton(self, text=CONNECTED_TEXT, font=("Roboto", 12), - fg_color="transparent", text_color=CONNECTED_COLOR, hover=False, width=70, command=self.toggle_rpc) - self.discord_button.pack(pady=(20, 0)) + self.discord_button = ctk.CTkButton(self, text="Connecting...", font=("Roboto", 12), fg_color="transparent", + text_color=CONNECTED_COLOR, width=70, command=self.toggle_rpc, + hover_color = self.cget("bg_color"), state="disabled") + # Make text change color on hover + self.discord_button.bind("", lambda event: self.discord_button.configure(text_color=CONNECTED_HOVER + if self.discord_button.cget("text") is not DISCONNECTED_TEXT + else DISCONNECTED_HOVER)) + self.discord_button.bind("", lambda event: self.discord_button.configure(text_color=CONNECTED_COLOR + if self.discord_button.cget("text") is not DISCONNECTED_TEXT + else DISCONNECTED_COLOR)) + self.discord_button.pack(pady=(27, 0)) def initialize_state(self, config): self.running = False @@ -79,6 +92,7 @@ def initialize_state(self, config): def initialize_rpc(self): self.rpc = RichPresence() + self.discord_button.configure(state="normal", text=CONNECTED_TEXT) self.rpc_thread = threading.Thread(target=self.update_rpc, daemon=True) self.rpc_thread.start() From 1b6065ae980458c3e70c41a5e71a9d0945b6b397 Mon Sep 17 00:00:00 2001 From: freeram Date: Tue, 7 May 2024 03:40:46 -0600 Subject: [PATCH 9/9] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0e41038..d3ed744 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +CTkMessagebox==2.5 customtkinter==5.2.2 matplotlib==3.8.3 pygame==2.5.2