diff --git a/src/app.py b/src/app.py index e7df8e0..9ca1abd 100644 --- a/src/app.py +++ b/src/app.py @@ -38,6 +38,7 @@ def __init__(self): super().__init__() self.title("Pomodoro Tracker") self.geometry(f"{PomodoroApp.WIDTH}x{PomodoroApp.HEIGHT}") + self.resizable(False, True) self.tabview = TabView(master=self) self.tabview.pack(pady=(15, 30), expand=True, fill='y') diff --git a/src/frames/pomodoro_frame.py b/src/frames/pomodoro_frame.py index 18b41d4..db04830 100644 --- a/src/frames/pomodoro_frame.py +++ b/src/frames/pomodoro_frame.py @@ -1,21 +1,18 @@ import time import threading import customtkinter as ctk -from CTkMessagebox import CTkMessagebox from datetime import datetime, timedelta +from CTkMessagebox import CTkMessagebox 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" @@ -25,47 +22,80 @@ class PomodoroFrame(ctk.CTkFrame): def __init__(self, master): super().__init__(master) config = load_config() - self.initialize_ui(config) self.initialize_state(config) threading.Thread(target=self.initialize_rpc, daemon=True).start() def initialize_ui(self, config): # Helper text that appears when a break is running - self.break_text = ctk.StringVar(value="") - self.break_label = ctk.CTkLabel(self, textvariable=self.break_text, font=("Roboto", 15)) - self.break_label.pack(pady=(5, 0)) + self.break_text = ctk.StringVar(value="") + self.break_label = ctk.CTkLabel(self, + textvariable=self.break_text, + font=("Roboto", 15)) # Display self.pomodoro_time = config.get("pomodoro_time", DEF_POMODORO_MINS) * 60 minutes, seconds = divmod(self.pomodoro_time, 60) - self.timer_display = ctk.CTkLabel(self, text=f"{minutes:02d}:{seconds:02d}", font=("Helvetica", 58)) - self.timer_display.pack(pady=(15, 41)) + self.timer_display = ctk.CTkLabel(self, + text=f"{minutes:02d}:{seconds:02d}", + font=("Helvetica", 58)) # Controls - self.start_button = ctk.CTkButton(self, text="Start", font=("Roboto", 17), border_width=2, command=self.toggle_timer) + self.start_button = ctk.CTkButton(self, + text="Start", + font=("Roboto", 17), + border_width=2, + command=self.toggle_timer) + # Used in toggle_timer() self.start_color = self.start_button.cget("fg_color") - self.start_button.pack(pady=(0, 10)) - self.sb_button = ctk.CTkButton(self, text="Short break", font=("Roboto", 17), fg_color=BREAK_BTN_COLOR, hover_color=BREAK_HOVER, command=self.short_break) - self.sb_button.pack(pady=(0, 10)) + self.sb_button = ctk.CTkButton(self, + text="Short break", + font=("Roboto", 17), + fg_color=BREAK_BTN_COLOR, + hover_color=BREAK_HOVER, + command=self.short_break) + + self.lb_button = ctk.CTkButton(self, + text="Long break", + font=("Roboto", 17), + fg_color=BREAK_BTN_COLOR, + hover_color=BREAK_HOVER, + command=self.long_break) + + self.reset_button = ctk.CTkButton(self, + text="Reset", + font=("Roboto", 17), + fg_color=RESET_BTN_COLOR, + hover_color=RESET_HOVER, + command=self.reset) + + self.discord_button = ctk.CTkButton(self, + text="Connecting...", + font=("Roboto", 12), + fg_color="transparent", + text_color=CONNECTED_COLOR, + width=70, + hover_color=self.cget("bg_color"), + command=self.toggle_rpc, + state="disabled") - self.lb_button = ctk.CTkButton(self, text="Long break", font=("Roboto", 17), fg_color=BREAK_BTN_COLOR, hover_color=BREAK_HOVER, command=self.long_break) - self.lb_button.pack(pady=(0, 10)) + # Make text change color on hover + self.discord_button.bind("", + lambda e: 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 e: self.discord_button.configure(text_color=CONNECTED_COLOR + if self.discord_button.cget("text") is not DISCONNECTED_TEXT + else DISCONNECTED_COLOR)) - self.reset_button = ctk.CTkButton(self, text="Reset", font=("Roboto", 17), fg_color=RESET_BTN_COLOR, hover_color=RESET_HOVER, command=self.reset) + self.break_label.pack(pady=(5, 0)) + self.timer_display.pack(pady=(15, 41)) + self.start_button.pack(pady=(0, 10)) + self.sb_button.pack(pady=(0, 10)) + self.lb_button.pack(pady=(0, 10)) 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): @@ -110,14 +140,14 @@ def connect_rpc(self): 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") + CTkMessagebox(title="Error", message="Connecting to Discord failed. Check 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") + CTkMessagebox(title="Error", message="Disconnecting from Discord failed. Check console for error output", icon="cancel") def update_rpc(self): while True: @@ -130,9 +160,8 @@ def update_rpc(self): self.rpc.paused_state(self.start_time_timestamp) else: 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) + elif self.discord_button.cget("state") == 'normal': + self.discord_button.configure(text=DISCONNECTED_TEXT, text_color=DISCONNECTED_COLOR) # Discord-imposed rate limit time.sleep(15) @@ -140,53 +169,64 @@ def update_rpc(self): def toggle_timer(self): if self.next_timer_update: self.after_cancel(self.next_timer_update) - self.running = not self.running btn_text = "Pause" if self.running else "Resume" btn_fg = "transparent" if self.running else self.start_color self.start_button.configure(text=btn_text, fg_color=btn_fg, hover=not self.running) - # Rich presence info + # Tracking time studied today in track_second() now = datetime.now() + self.current_date = now.strftime("%Y-%m-%d") + # Rich presence info end_time = now + timedelta(seconds=self.remaining_time) self.start_time_timestamp = now.timestamp() self.end_time_timestamp = end_time.timestamp() self.paused = not self.running - # For tracking seconds studied by date - self.current_date = now.strftime("%Y-%m-%d") self.update_timer() def update_timer(self): if self.running and self.remaining_time > 0: + self.next_timer_update = self.after(1000, self.update_timer) self.remaining_time -= 1 if not self.break_running: self.track_second() minutes, seconds = divmod(self.remaining_time, 60) self.timer_display.configure(text=f"{minutes:02d}:{seconds:02d}") - self.next_timer_update = self.after(1000, self.update_timer) elif self.remaining_time == 0: self.session_ended() def track_second(self): - current_date = self.current_date self.seconds_studied += 1 data = load_data() data['total_seconds_studied'] += 1 - data['seconds_by_date'][current_date] = data['seconds_by_date'].get(current_date, 0) + 1 + data['seconds_by_date'][self.current_date] = data['seconds_by_date'].get(self.current_date, 0) + 1 save_data(data) - def reset(self, to:str="pomodoro_time", default:int=DEF_POMODORO_MINS): + def session_ended(self): + self.short_break_counter += 1 if self.short_break_running else 0 + was_break = self.break_running + self.session_counter += 1 if not was_break else 0 + self.reset() + beep.play() + + if was_break and self.auto_break_cycling: + self.toggle_timer() + elif not was_break: + self.pomodoro_complete() + + def reset(self, to: str = "pomodoro_time", default: int = DEF_POMODORO_MINS): + if self.next_timer_update: + self.after_cancel(self.next_timer_update) + self.next_timer_update = None + self.running = False self.break_running = False self.short_break_running = False self.paused = False self.break_text.set("") - if self.next_timer_update: - self.after_cancel(self.next_timer_update) - self.next_timer_update = None - + # Load current settings config = load_config() self.pomodoro_time = int(config.get(to, default) * 60) self.auto_break_cycling = config.get("auto_break_cycling", False) @@ -199,28 +239,13 @@ def reset(self, to:str="pomodoro_time", default:int=DEF_POMODORO_MINS): self.timer_display.configure(text=f"{minutes:02d}:{seconds:02d}") self.start_button.configure(text="Start", fg_color=self.start_color) - def session_ended(self): - # TODO: this function looks vile - was_break = self.break_running - was_short_break = self.short_break_running - self.reset() - beep.play() - - self.short_break_counter += 1 if was_short_break else 0 - - if was_break: - if self.auto_break_cycling: - self.toggle_timer() - return - - self.session_counter += 1 - - data = load_data() + def pomodoro_complete(self): + data = load_data() data['total_pomodoro_sessions'] += 1 data['sessions_by_date'][self.current_date] = data['sessions_by_date'].get(self.current_date, 0) + 1 save_data(data) - if not was_break and self.auto_break_cycling: + if self.auto_break_cycling: if self.short_break_counter >= self.short_breaks_before_long: self.short_break_counter = 0 self.long_break() @@ -233,7 +258,7 @@ def short_break(self): self.short_break_running = True self.break_text.set("Short break") self.toggle_timer() - + def long_break(self): self.reset(to="long_break_time", default=DEF_LB_MINS) self.break_running = True diff --git a/src/logic/richpresence.py b/src/logic/richpresence.py index 7d63834..1a1e688 100644 --- a/src/logic/richpresence.py +++ b/src/logic/richpresence.py @@ -57,19 +57,19 @@ def format_time(self, seconds_studied): @_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") + details="Idling", start=self.launch_time, large_image="graytomato", large_text="github.com/jake158/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") + end=end_time, large_image="tomato", large_text="github.com/jake158/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") + large_text="github.com/jake158/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") + end=end_time, large_image="greentomato", large_text="github.com/jake158/pomodoro-discord") diff --git a/src/utils.py b/src/utils.py index 5cfa788..a7f2df0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -5,7 +5,7 @@ mixer.init() -beep = mixer.Sound('src/assets/sounds/beep.mp3') +beep = mixer.Sound(os.path.join('src', 'assets', 'sounds', 'beep.mp3')) DEF_POMODORO_MINS = 25 @@ -13,9 +13,9 @@ DEF_LB_MINS = 15 DEF_SB_BEFORE_L = 3 -CONFIG_FILE = 'config.json' -DATA_FILE = 'data.json' -THEMES_DIR = 'src/assets/themes' +CONFIG_FILE = os.path.join('config.json') +DATA_FILE = os.path.join('data.json') +THEMES_DIR = os.path.join('src', 'assets', 'themes') def load_file(filename, on_no_file=None):