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 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..18b41d4 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -1,15 +1,25 @@ 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 BREAK_BTN_COLOR = "#9a9a9a" BREAK_HOVER = "#adaaaa" + RESET_BTN_COLOR = "#cca508" RESET_HOVER = "#e3b707" +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): def __init__(self, master): @@ -44,7 +54,19 @@ 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="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 @@ -70,19 +92,47 @@ 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() + def toggle_rpc(self): + self.discord_button.configure(state="disabled") + if not self.rpc.connected: + self.discord_button.configure(text="Connecting...") + threading.Thread(target=self.connect_rpc, daemon=True).start() + else: + 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_TEXT, text_color=CONNECTED_COLOR, state="normal") + else: + 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=DISCONNECTED_TEXT, text_color=DISCONNECTED_COLOR, state="normal") + else: + 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): 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) + if self.rpc.connected: + 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(self.seconds_studied) else: - self.rpc.idling_state() + 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) 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()) diff --git a/src/logic/richpresence.py b/src/logic/richpresence.py index db1526f..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' @@ -9,33 +10,66 @@ 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() + self.total_seconds_studied = load_data().get('total_seconds_studied', 0) + 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: - self.connect() - self.idling_state() + super().connect() + self.connected = True + return True except Exception as e: print(f"Failed to connect to Discord: {e}") + self.connected = False + return False + + def disconnect(self): + try: + super().close() + self.connected = False + return True + except Exception as e: + print(f"Failed to disconnect from Discord: {e}") + self.connected = True + 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: return f"{round(total_hours, 1) if total_hours % 1 != 0 else int(total_hours)} hours" - 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 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): 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")