import tkinter as tk from tkinter import ttk, messagebox import ttkbootstrap as ttk from ttkbootstrap.constants import * from tkinter import scrolledtext import pytz from datetime import datetime, timedelta, time import threading import time as t import pygame import json import os import sys from ctypes import windll from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from google.auth.transport.requests import Request # Initialize pygame mixer for sound playing pygame.mixer.init() # Google Calendar API scope and token file SCOPES = ['https://www.googleapis.com/auth/calendar'] TOKEN_FILE = 'token.json' # Initialize the main window with a modern theme root = ttk.Window(themename="superhero") # Change theme if needed root.title("Task Tracker") root.geometry("1500x950") # Ensure the window appears in the taskbar and remove the default window frame root.attributes('-fullscreen', True) # Not full screen, but this helps manage taskbar appearance # Function to minimize the window def minimize_window(): root.iconify() # Function to maximize or restore the window size is_maximized = False def toggle_maximize_restore(): global is_maximized if not is_maximized: root.state('zoomed') # Maximize window is_maximized = True else: root.state('normal') # Restore to original size is_maximized = False # Function to close the window def close_window(): root.quit() # Create a custom title bar frame at the top title_bar = ttk.Frame(root, bootstyle="primary", relief='raised', height=30) title_bar.grid(row=0, column=0, columnspan=4, sticky="ew") # Add a title label title_label = ttk.Label(title_bar, text="Task Tracker", bootstyle="inverse-primary") title_label.pack(side="left", padx=10) # Add minimize, maximize, and close buttons in the correct order close_button = ttk.Button(title_bar, text="X", command=close_window, bootstyle="danger-outline") close_button.pack(side="right", padx=5, pady=2) minimize_button = ttk.Button(title_bar, text="_", command=minimize_window, bootstyle="warning-outline") minimize_button.pack(side="right", padx=5, pady=2) # Variables to track window movement x_offset = 0 y_offset = 0 # Functions to move the window def get_window_position(event): global x_offset, y_offset x_offset = event.x y_offset = event.y def move_window(event): root.geometry(f'+{event.x_root - x_offset}+{event.y_root - y_offset}') # Bind the title bar to allow dragging the window title_bar.bind("", get_window_position) title_bar.bind("", move_window) # Function to create a rounded rectangle in a canvas def round_rectangle(canvas, x1, y1, x2, y2, radius=25, **kwargs): points = [x1 + radius, y1, x1 + radius, y1, x2 - radius, y1, x2 - radius, y1, x2, y1, x2, y1 + radius, x2, y1 + radius, x2, y2 - radius, x2, y2 - radius, x2, y2, x2 - radius, y2, x2 - radius, y2, x1 + radius, y2, x1 + radius, y2, x1, y2, x1, y2 - radius, x1, y2 - radius, x1, y1 + radius, x1, y1 + radius, x1, y1] return canvas.create_polygon(points, **kwargs, smooth=True) # Create a canvas and use grid to place it in the window canvas = tk.Canvas(root, width=300, height=300, bg="blue", bd=0, highlightthickness=0) canvas.grid(row=1, column=0, sticky="nsew") # Configure the grid to allow the canvas to expand and fill the window root.grid_rowconfigure(1, weight=1) root.grid_columnconfigure(0, weight=1) # Define a list to store tasks and their completion status tasks = [] alarm_playing = False # Global flag to stop alarm # Path to the task file TASK_FILE = "tasks.json" # Path to the alarm sound file (ensure you have a sound file like "alarm.wav" in the directory) ALARM_SOUND_FILE = "alarm.wav" # Global variable to store Google Calendar API service google_service = None # Global dictionary to store event IDs from Google Calendar task_event_ids = {} def play_alarm_sound(): global alarm_playing if not alarm_playing: try: print("Playing alarm sound on a new thread...") threading.Thread(target=_play_alarm_sound_worker).start() except Exception as e: print(f"Error starting sound thread: {e}") def _play_alarm_sound_worker(): global alarm_playing try: pygame.mixer.music.load(ALARM_SOUND_FILE) pygame.mixer.music.play(-1) # Loop the sound indefinitely alarm_playing = True except Exception as e: print(f"Error playing sound: {e}") # Function to stop the alarm sound def stop_alarm(): global alarm_playing if alarm_playing: print("Stopping alarm...") pygame.mixer.music.stop() # Stop the sound alarm_playing = False # Function to save tasks to a JSON file (including in-progress flag) def save_tasks(): try: with open(TASK_FILE, "w") as f: # Convert datetime objects to ISO format strings for JSON serialization tasks_serializable = [] for task in tasks: task_list = list(task) # Convert datetime objects to strings if task_list[14]: task_list[14] = task_list[14].isoformat() if task_list[15]: task_list[15] = task_list[15].isoformat() # Convert time objects to strings if task_list[2]: task_list[2] = task_list[2].isoformat() if task_list[3]: task_list[3] = task_list[3].isoformat() if task_list[7]: task_list[7] = task_list[7].isoformat() if task_list[9]: task_list[9] = task_list[9].isoformat() if task_list[11]: task_list[11] = task_list[11].isoformat() # event_id does not need conversion, but ensure it's included tasks_serializable.append(task_list) json.dump(tasks_serializable, f) except Exception as e: print(f"Error saving tasks: {e}") # Function to load tasks from a JSON file (including in-progress flag) def load_tasks(): global tasks if os.path.exists(TASK_FILE): try: with open(TASK_FILE, "r") as f: loaded_tasks = json.load(f) tasks = [] for task in loaded_tasks: task_data = ( task[0], # Task name task[1], # Notes time.fromisoformat(task[2]) if task[2] else None, # Complete at time time.fromisoformat(task[3]) if task[3] else None, # Complete by time task[4], # Daily task[5], # Weekly task[6], # Monthly time.fromisoformat(task[7]) if task[7] else None, # Daily time task[8], # Weekly day time.fromisoformat(task[9]) if task[9] else None, # Weekly time task[10], # Monthly day time.fromisoformat(task[11]) if task[11] else None, # Monthly time task[12], # Completed task[13], # In-progress datetime.fromisoformat(task[14]) if task[14] else None, # Complete at datetime datetime.fromisoformat(task[15]) if task[15] else None, # Complete by datetime task[16] if len(task) > 16 else None # event_id (Handle backward compatibility) ) tasks.append(task_data) update_task_listbox() # Load tasks into the listboxes except Exception as e: print(f"Error loading tasks: {e}") # Function to show notification after alarm sound def show_alarm_popup(task, time_type): messagebox.showinfo("Task Alarm", f"It's time to {time_type} for task: {task}") # Function to trigger alarm (plays sound first, then popup) def trigger_alarm(task, time_type): global alarm_playing # If the alarm is not already playing, start playing it in a new thread if not alarm_playing: print(f"Triggering alarm for task '{task}' at {time_type}") play_alarm_sound() # This will run in a separate thread # Show the task alarm popup using Tkinter's event loop root.after(0, lambda: show_alarm_popup(task, time_type)) # Function to convert hour, minute, AM/PM to a time object def get_time_from_dropdown(hour, minute, am_pm): hour = int(hour) minute = int(minute) if am_pm == "PM" and hour != 12: hour += 12 if am_pm == "AM" and hour == 12: hour = 0 return time(hour, minute) # Function to enable/disable options based on selection def update_state(): # Reset all controls (enable all checkboxes and disable all time fields by default) daily_checkbox.config(state=tk.NORMAL) weekly_checkbox.config(state=tk.NORMAL) monthly_checkbox.config(state=tk.NORMAL) at_radio_button.config(state=tk.NORMAL) by_radio_button.config(state=tk.NORMAL) at_date_checkbox.config(state=tk.DISABLED) by_date_checkbox.config(state=tk.DISABLED) # Disable time selectors initially daily_hour_combo.config(state=tk.DISABLED) daily_minute_combo.config(state=tk.DISABLED) daily_am_pm_combo.config(state=tk.DISABLED) weekly_day_combo.config(state=tk.DISABLED) weekly_hour_combo.config(state=tk.DISABLED) weekly_minute_combo.config(state=tk.DISABLED) weekly_am_pm_combo.config(state=tk.DISABLED) monthly_day_combo.config(state=tk.DISABLED) monthly_hour_combo.config(state=tk.DISABLED) monthly_minute_combo.config(state=tk.DISABLED) monthly_am_pm_combo.config(state=tk.DISABLED) at_hour_combo.config(state=tk.DISABLED) at_minute_combo.config(state=tk.DISABLED) at_am_pm_combo.config(state=tk.DISABLED) by_hour_combo.config(state=tk.DISABLED) by_minute_combo.config(state=tk.DISABLED) by_am_pm_combo.config(state=tk.DISABLED) # Handle mutually exclusive behavior between Daily, Weekly, Monthly, Complete at, and Complete by # If Daily is selected if daily_var.get(): # Disable Weekly, Monthly, Complete at, and Complete by weekly_checkbox.config(state=tk.DISABLED) monthly_checkbox.config(state=tk.DISABLED) at_radio_button.config(state=tk.DISABLED) by_radio_button.config(state=tk.DISABLED) # Enable Daily time selectors daily_hour_combo.config(state=tk.NORMAL) daily_minute_combo.config(state=tk.NORMAL) daily_am_pm_combo.config(state=tk.NORMAL) # If Weekly is selected elif weekly_var.get(): # Disable Daily, Monthly, Complete at, and Complete by daily_checkbox.config(state=tk.DISABLED) monthly_checkbox.config(state=tk.DISABLED) at_radio_button.config(state=tk.DISABLED) by_radio_button.config(state=tk.DISABLED) # Enable Weekly time and day selectors weekly_day_combo.config(state=tk.NORMAL) weekly_hour_combo.config(state=tk.NORMAL) weekly_minute_combo.config(state=tk.NORMAL) weekly_am_pm_combo.config(state=tk.NORMAL) # If Monthly is selected elif monthly_var.get(): # Disable Daily, Weekly, Complete at, and Complete by daily_checkbox.config(state=tk.DISABLED) weekly_checkbox.config(state=tk.DISABLED) at_radio_button.config(state=tk.DISABLED) by_radio_button.config(state=tk.DISABLED) # Enable Monthly day and time selectors monthly_day_combo.config(state=tk.NORMAL) monthly_hour_combo.config(state=tk.NORMAL) monthly_minute_combo.config(state=tk.NORMAL) monthly_am_pm_combo.config(state=tk.NORMAL) # If Complete at is selected (it does not disable Complete by) if at_radio_var.get(): # Disable Daily, Weekly, and Monthly daily_checkbox.config(state=tk.DISABLED) weekly_checkbox.config(state=tk.DISABLED) monthly_checkbox.config(state=tk.DISABLED) # Enable Complete at time selectors and Add Date checkbox at_hour_combo.config(state=tk.NORMAL) at_minute_combo.config(state=tk.NORMAL) at_am_pm_combo.config(state=tk.NORMAL) at_date_checkbox.config(state=tk.NORMAL) # If "Add Date" for Complete at is checked, show the date selectors if at_date_var.get(): at_date_frame.grid() else: at_date_frame.grid_remove() # If Complete by is selected (it does not disable Complete at) if by_radio_var.get(): # Disable Daily, Weekly, and Monthly daily_checkbox.config(state=tk.DISABLED) weekly_checkbox.config(state=tk.DISABLED) monthly_checkbox.config(state=tk.DISABLED) # Enable Complete by time selectors and Add Date checkbox by_hour_combo.config(state=tk.NORMAL) by_minute_combo.config(state=tk.NORMAL) by_am_pm_combo.config(state=tk.NORMAL) by_date_checkbox.config(state=tk.NORMAL) # If "Add Date" for Complete by is checked, show the date selectors if by_date_var.get(): by_date_frame.grid() else: by_date_frame.grid_remove() # Ensure the date frames are hidden if neither "Add Date" checkbox is checked if not at_date_var.get(): at_date_frame.grid_remove() if not by_date_var.get(): by_date_frame.grid_remove() # If neither "Complete at" nor "Complete by" is selected, ensure both date checkboxes and frames are disabled if not at_radio_var.get() and not by_radio_var.get(): at_date_checkbox.config(state=tk.DISABLED) by_date_checkbox.config(state=tk.DISABLED) at_date_frame.grid_remove() by_date_frame.grid_remove() # Initialize the states to make sure the correct controls are disabled on start def initialize_state(): # Disable the "Add Date" checkboxes and date frames initially at_date_checkbox.config(state=tk.DISABLED) by_date_checkbox.config(state=tk.DISABLED) # Disable time selectors initially at_hour_combo.config(state=tk.DISABLED) at_minute_combo.config(state=tk.DISABLED) at_am_pm_combo.config(state=tk.DISABLED) by_hour_combo.config(state=tk.DISABLED) by_minute_combo.config(state=tk.DISABLED) by_am_pm_combo.config(state=tk.DISABLED) # Disable recurrence time selectors initially daily_hour_combo.config(state=tk.DISABLED) daily_minute_combo.config(state=tk.DISABLED) daily_am_pm_combo.config(state=tk.DISABLED) weekly_day_combo.config(state=tk.DISABLED) weekly_hour_combo.config(state=tk.DISABLED) weekly_minute_combo.config(state=tk.DISABLED) weekly_am_pm_combo.config(state=tk.DISABLED) monthly_day_combo.config(state=tk.DISABLED) monthly_hour_combo.config(state=tk.DISABLED) monthly_minute_combo.config(state=tk.DISABLED) monthly_am_pm_combo.config(state=tk.DISABLED) # Function to get the path to the credentials.json file def get_credentials_path(): """ Returns the correct path to the 'credentials.json' file depending on whether the script is running as a standalone executable (PyInstaller) or as a Python script. """ if hasattr(sys, '_MEIPASS'): # If bundled with PyInstaller, look for the file in the _MEIPASS folder return os.path.join(sys._MEIPASS, 'credentials.json') else: # When running normally as a script, use the current working directory return os.path.join(os.getcwd(), 'credentials.json') # Function to handle Google Login def google_login(): creds = None SCOPES = ['https://www.googleapis.com/auth/calendar'] credentials_path = get_credentials_path() # Get the path to credentials.json if not os.path.exists(credentials_path): # If credentials.json is missing, show an error message root.after(0, lambda: messagebox.showerror("Credentials Missing", "credentials.json file not found in the program directory.")) return # Load credentials from the token.json file, if they exist token_path = 'token.json' if os.path.exists(token_path): with open(token_path, 'r') as token_file: creds = Credentials.from_authorized_user_info(json.load(token_file), SCOPES) # If there are no valid credentials, let the user log in if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: try: creds.refresh(Request()) # Refresh credentials if they're expired except Exception as e: root.after(0, lambda: messagebox.showerror("Login Error", f"Failed to refresh credentials: {e}")) return else: # If no credentials are available, prompt the user to log in try: flow = InstalledAppFlow.from_client_secrets_file(credentials_path, SCOPES) creds = flow.run_local_server(port=0) except Exception as e: root.after(0, lambda: messagebox.showerror("Login Error", f"Failed to authenticate using Google: {e}")) return # Save the credentials for the next run with open(token_path, 'w') as token_file: token_file.write(creds.to_json()) # Now that we have valid credentials, build the Google Calendar service try: global google_service google_service = build('calendar', 'v3', credentials=creds) root.after(0, lambda: messagebox.showinfo("Login Success", "Successfully logged in to Google Calendar!")) except Exception as e: root.after(0, lambda: messagebox.showerror("Login Error", f"Failed to connect to Google Calendar: {e}")) # Get the local time zone (you can change this to a specific time zone if needed) local_tz = pytz.timezone('America/New_York') # Replace 'America/New_York' with your local time zone # Function to add a task to Google Calendar with recurrence def add_task_to_google_calendar(task, start_time, notes=None, recurrence=None, duration_minutes=60): """ Add a task to Google Calendar with a start time, optional recurrence, and duration. """ if google_service is None: messagebox.showerror("Not Logged In", "Please log in to Google Calendar first!") return # Convert start_time to the user's local time zone start_time = local_tz.localize(start_time) # Calculate end time by adding the duration to the start time (only if duration is provided) if duration_minutes is not None: end_time = start_time + timedelta(minutes=duration_minutes) else: end_time = start_time + timedelta(minutes=60) # Default to 60 minutes if no duration is specified # Create the basic event structure event = { 'summary': task, 'description': notes, 'start': { 'dateTime': start_time.isoformat(), # Ensure time zone info is included 'timeZone': str(local_tz), # Explicitly set the user's time zone }, 'end': { 'dateTime': end_time.isoformat(), # Ensure time zone info is included 'timeZone': str(local_tz), # Explicitly set the user's time zone }, 'reminders': { 'useDefault': False, 'overrides': [ {'method': 'popup', 'minutes': 10}, # Popup reminder 10 minutes before the task ], } } # Add recurrence if provided if recurrence: event['recurrence'] = recurrence try: # Insert the event into Google Calendar event = google_service.events().insert(calendarId='primary', body=event).execute() print(f'Event created: {event.get("htmlLink")}') return event['id'] # Return the event ID to store it except Exception as e: print(f"An error occurred: {e}") messagebox.showerror("Error", f"Failed to add task to Google Calendar: {e}") return None # Function to remove task from Google Calendar def remove_task_from_google_calendar(event_id): if google_service is None: messagebox.showerror("Not Logged In", "Please log in to Google Calendar first!") return try: google_service.events().delete(calendarId='primary', eventId=event_id).execute() print(f"Event with ID {event_id} deleted successfully from Google Calendar.") except Exception as e: print(f"An error occurred while deleting task: {e}") messagebox.showerror("Error", f"Failed to delete task from Google Calendar: {e}") # Function to convert hour, minute, AM/PM to a time object def get_time_from_dropdown(hour, minute, am_pm): hour = int(hour) minute = int(minute) if am_pm == "PM" and hour != 12: hour += 12 if am_pm == "AM" and hour == 12: hour = 0 return time(hour, minute) # Define checkboxes for date selection at_date_var = tk.IntVar() by_date_var = tk.IntVar() # Function to add a task with a "Complete at" or "Complete by" time def add_task(): task = task_entry.get().strip() # Get the task name from the entry field notes = notes_entry.get("1.0", tk.END).strip() # Get the notes from the Text widget complete_at_selected = at_radio_var.get() complete_by_selected = by_radio_var.get() complete_at_time = None complete_by_time = None start_time = None end_time = None # Used for "Complete by" and duration calculations duration_minutes = 60 # Default duration complete_at_datetime = None complete_by_datetime = None # Check for "Complete at" selection and retrieve the time if complete_at_selected: complete_at_time = get_time_from_dropdown(at_hour_combo.get(), at_minute_combo.get(), at_am_pm_combo.get()) if at_date_var.get(): # If "Add Date" checkbox is checked selected_date = datetime( int(at_year_combo.get()), int(at_month_combo.get()), int(at_day_combo.get()) ).date() else: selected_date = datetime.now().date() # Default to today's date start_time = datetime.combine(selected_date, complete_at_time) complete_at_datetime = start_time # Store the datetime object # Check for "Complete by" selection and retrieve the time if complete_by_selected: complete_by_time = get_time_from_dropdown(by_hour_combo.get(), by_minute_combo.get(), by_am_pm_combo.get()) if by_date_var.get(): # If "Add Date" checkbox is checked selected_date = datetime( int(by_year_combo.get()), int(by_month_combo.get()), int(by_day_combo.get()) ).date() else: selected_date = datetime.now().date() # Default to today's date end_time = datetime.combine(selected_date, complete_by_time) complete_by_datetime = end_time # Store the datetime object # Calculate duration if both Complete at and Complete by are selected if complete_at_selected: duration = end_time - start_time duration_minutes = int(duration.total_seconds() / 60) # Convert duration to minutes else: # If only Complete by is selected, set start_time based on default duration start_time = end_time - timedelta(minutes=duration_minutes) # Handle recurrence (daily, weekly, monthly) daily = daily_var.get() weekly = weekly_var.get() monthly = monthly_var.get() # Get time for recurrence options (if selected) daily_time = get_time_from_dropdown(daily_hour_combo.get(), daily_minute_combo.get(), daily_am_pm_combo.get()) if daily else None weekly_day = weekly_day_combo.get() if weekly else None weekly_time = get_time_from_dropdown(weekly_hour_combo.get(), weekly_minute_combo.get(), weekly_am_pm_combo.get()) if weekly else None monthly_day = monthly_day_combo.get() if monthly else None monthly_time = get_time_from_dropdown(monthly_hour_combo.get(), monthly_minute_combo.get(), monthly_am_pm_combo.get()) if monthly else None event_id = None # Initialize event_id if daily: # Create a datetime object for daily tasks using the selected time for today start_time = datetime.combine(datetime.now().date(), daily_time) if weekly: # For weekly tasks, calculate the next occurrence of the selected day day_mapping = {"Monday": "MO", "Tuesday": "TU", "Wednesday": "WE", "Thursday": "TH", "Friday": "FR", "Saturday": "SA", "Sunday": "SU"} days_ahead = (["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].index(weekly_day) - datetime.now().weekday()) % 7 next_weekly_date = datetime.now() + timedelta(days=days_ahead) # Combine the date of the next occurrence with the selected time start_time = datetime.combine(next_weekly_date.date(), weekly_time) if monthly: # For monthly tasks, calculate the next occurrence of the selected day of the month today = datetime.now() current_month = today.month current_year = today.year # Adjust if the selected day is not valid for the month try: if today.day > int(monthly_day): if current_month == 12: # Handle December to January rollover next_month_date = datetime(current_year + 1, 1, int(monthly_day)) else: next_month_date = datetime(current_year, current_month + 1, int(monthly_day)) else: # If the day is in the future, schedule for the current month next_month_date = datetime(current_year, current_month, int(monthly_day)) except ValueError: messagebox.showerror("Invalid Date", "The selected day is invalid for the current month.") return # Combine the date of the selected day with the selected time start_time = datetime.combine(next_month_date.date(), monthly_time) # Validate the task before adding it if task: # Add the task to Google Calendar if google_service: if daily: event_id = add_task_to_google_calendar(task, start_time, notes, recurrence=['RRULE:FREQ=DAILY'], duration_minutes=duration_minutes) elif weekly: day_code = day_mapping[weekly_day] # Get the correct day code for the recurrence rule event_id = add_task_to_google_calendar(task, start_time, notes, recurrence=[f'RRULE:FREQ=WEEKLY;BYDAY={day_code}'], duration_minutes=duration_minutes) elif monthly: event_id = add_task_to_google_calendar(task, start_time, notes, recurrence=[f'RRULE:FREQ=MONTHLY;BYMONTHDAY={monthly_day}'], duration_minutes=duration_minutes) elif complete_at_selected or complete_by_selected: event_id = add_task_to_google_calendar(task, start_time, notes, duration_minutes=duration_minutes) else: if messagebox.askyesno("Not Logged In", "You are not logged in to Google Calendar. Do you want to add the task without syncing to Google Calendar?"): pass else: return # Append the task to the tasks list, including the event_id tasks.append(( task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, False, False, complete_at_datetime, complete_by_datetime, event_id # Include event_id here )) update_task_listbox() # Update the UI task_entry.delete(0, tk.END) # Clear the task entry field notes_entry.delete("1.0", tk.END) # Clear the notes entry field save_tasks() # Save the task list else: messagebox.showwarning("Input Error", "Task cannot be empty!") # Function to show the notes for the selected task in a separate window def show_notes(event): listbox = event.widget selected_index = listbox.curselection() if selected_index: # Fetch the original index based on the listbox type (Main, Daily, Weekly, Monthly) if listbox == task_listbox: task_index = main_task_indices[selected_index[0]] elif listbox == daily_task_listbox: task_index = daily_task_indices[selected_index[0]] elif listbox == weekly_task_listbox: task_index = weekly_task_indices[selected_index[0]] elif listbox == monthly_task_listbox: task_index = monthly_task_indices[selected_index[0]] task_details = tasks[task_index] notes = task_details[1] # Get the notes associated with the task if notes.strip(): # Only show the window if there are notes # Create a new window to show the notes notes_window = tk.Toplevel(root) notes_window.title(f"Notes for {task_details[0]}") # Enable word wrapping in the Text widget by setting wrap="word" notes_text = tk.Text(notes_window, height=10, width=40, wrap="word") notes_text.pack(padx=10, pady=10) notes_text.insert(tk.END, notes) notes_text.config(state=tk.DISABLED) # Make the notes read-only else: messagebox.showinfo("No Notes", f"There are no notes for the task: {task_details[0]}") # Function to mark a task as in-progress and stop the alarm if it's playing def mark_task_in_progress(): selected_task_index = task_listbox.curselection() selected_daily_task_index = daily_task_listbox.curselection() selected_weekly_task_index = weekly_task_listbox.curselection() selected_monthly_task_index = monthly_task_listbox.curselection() task_index = None # Check which listbox has a selected task if selected_task_index: task_index = main_task_indices[selected_task_index[0]] # Use the mapped main task index elif selected_daily_task_index: task_index = daily_task_indices[selected_daily_task_index[0]] # Use the mapped daily task index elif selected_weekly_task_index: task_index = weekly_task_indices[selected_weekly_task_index[0]] # Use the mapped weekly task index elif selected_monthly_task_index: task_index = monthly_task_indices[selected_monthly_task_index[0]] # Use the mapped monthly task index else: messagebox.showwarning("Selection Error", "Please select a task to mark as In-Progress") return if task_index is not None: task = tasks[task_index] # Mark the task as in-progress (set `in_progress` to True, `completed` to False) tasks[task_index] = ( task[0], # Task name task[1], # Notes task[2], # Complete at time task[3], # Complete by time task[4], # Daily flag task[5], # Weekly flag task[6], # Monthly flag task[7], # Daily time task[8], # Weekly day task[9], # Weekly time task[10], # Monthly day task[11], # Monthly time False, # Completed flag set to False True, # In-progress flag set to True task[14], # Complete at datetime task[15], # Complete by datetime task[16] # event_id (Include this) ) # Stop the alarm if it is playing stop_alarm() # Ensure alarm stops immediately when a task is marked in-progress # Update the listboxes to reflect the change update_task_listbox() save_tasks() # Save the updated tasks # Clear selection from all listboxes task_listbox.selection_clear(0, tk.END) daily_task_listbox.selection_clear(0, tk.END) weekly_task_listbox.selection_clear(0, tk.END) monthly_task_listbox.selection_clear(0, tk.END) print(f"Task '{task[0]}' marked as In-Progress.") # Debugging message else: print("No task index found for In-Progress.") # Debugging message # Function to mark a task as completed and stop the alarm if it's playing def mark_task_completed(): selected_task_index = task_listbox.curselection() selected_daily_task_index = daily_task_listbox.curselection() selected_weekly_task_index = weekly_task_listbox.curselection() selected_monthly_task_index = monthly_task_listbox.curselection() task_index = None # Check which listbox has a selected task if selected_task_index: task_index = main_task_indices[selected_task_index[0]] # Use the mapped main task index elif selected_daily_task_index: task_index = daily_task_indices[selected_daily_task_index[0]] # Use the mapped daily task index elif selected_weekly_task_index: task_index = weekly_task_indices[selected_weekly_task_index[0]] # Use the mapped weekly task index elif selected_monthly_task_index: task_index = monthly_task_indices[selected_monthly_task_index[0]] # Use the mapped monthly task index else: messagebox.showwarning("Selection Error", "Please select a task to mark as completed") return if task_index is not None: task_data = tasks[task_index] # Unpack task data (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed, in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data # Mark the task as completed (set `completed` to True and `in_progress` to False) tasks[task_index] = ( task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, True, False, complete_at_datetime, complete_by_datetime, event_id # Repack with new status ) # Stop the alarm if it is playing stop_alarm() # Ensure alarm stops immediately when a task is marked completed # Update the listboxes to reflect the change (it will remain in the list with a green checkmark) update_task_listbox() save_tasks() # Save the updated tasks print(f"Task '{task}' marked as completed.") # Debugging message # Initialize lists to map listbox selections to task indices daily_task_indices = [] weekly_task_indices = [] monthly_task_indices = [] main_task_indices = [] # Function to update the task listbox with colors and checkmarks for completed tasks # Initialize lists to map listbox selections to task indices daily_task_indices = [] weekly_task_indices = [] monthly_task_indices = [] main_task_indices = [] # Function to update the task listboxes with colors and checkmarks for completed tasks def update_task_listbox(): # Clear existing tasks from the listboxes daily_task_listbox.delete(0, tk.END) weekly_task_listbox.delete(0, tk.END) monthly_task_listbox.delete(0, tk.END) task_listbox.delete(0, tk.END) # Clear the main task listbox # Clear the index mapping lists daily_task_indices.clear() weekly_task_indices.clear() monthly_task_indices.clear() main_task_indices.clear() # Define checkmark and colors checkmark = '✓' in_progress_marker = '▼' incomplete_color = 'white' complete_color = 'lightgreen' in_progress_color = 'yellow' # Dark goldenrod color # Function to get the sorting key for tasks def get_sort_key(task_data): (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed, in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data now = datetime.now() if complete_at_datetime: return complete_at_datetime elif complete_by_datetime: return complete_by_datetime elif daily and daily_time: return datetime.combine(now.date(), daily_time) elif weekly and weekly_time: # Calculate the next occurrence of the weekly task days_ahead = (["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].index(weekly_day) - now.weekday()) % 7 next_weekly_date = now + timedelta(days=days_ahead) return datetime.combine(next_weekly_date.date(), weekly_time) elif monthly and monthly_time: # Calculate the next occurrence of the monthly task today = now current_month = today.month current_year = today.year monthly_day_int = int(monthly_day) try: if today.day > monthly_day_int: if current_month == 12: # Handle December to January rollover next_month_date = datetime(current_year + 1, 1, monthly_day_int) else: next_month_date = datetime(current_year, current_month + 1, monthly_day_int) else: next_month_date = datetime(current_year, current_month, monthly_day_int) return datetime.combine(next_month_date.date(), monthly_time) except ValueError: # If the day is invalid for the month (e.g., February 30), place it at the end return datetime.max else: return datetime.max # Tasks without a specific time are placed at the end # Build a list of tasks with their original indices tasks_with_indices = list(enumerate(tasks)) # Sort tasks by the calculated sort key while keeping the original indices tasks_sorted = sorted(tasks_with_indices, key=lambda x: get_sort_key(x[1])) # Loop through sorted tasks and populate the respective listboxes for task_index, task_data in tasks_sorted: if len(task_data) == 17: (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed, in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data else: continue # Skip if the task data doesn't have the right format # Decide the symbol and color based on the status if completed: task_str = f"{checkmark} {task}" color = complete_color elif in_progress: task_str = f"{in_progress_marker} {task}" color = in_progress_color else: task_str = task color = incomplete_color # Add task to the appropriate listbox if daily: task_str += f" [Daily at {daily_time.strftime('%I:%M %p')}]" daily_task_listbox.insert(tk.END, task_str) daily_task_listbox.itemconfig(tk.END, {'foreground': color}) daily_task_indices.append(task_index) elif weekly: task_str += f" [Weekly on {weekly_day} at {weekly_time.strftime('%I:%M %p')}]" weekly_task_listbox.insert(tk.END, task_str) weekly_task_listbox.itemconfig(tk.END, {'foreground': color}) weekly_task_indices.append(task_index) elif monthly: task_str += f" [Monthly on day {monthly_day} at {monthly_time.strftime('%I:%M %p')}]" monthly_task_listbox.insert(tk.END, task_str) monthly_task_listbox.itemconfig(tk.END, {'foreground': color}) monthly_task_indices.append(task_index) else: # For Main tasks (Complete at/by) if complete_at_datetime: task_str += f" (Complete at: {complete_at_datetime.strftime('%Y-%m-%d %I:%M %p')})" if complete_by_datetime: task_str += f" (Complete by: {complete_by_datetime.strftime('%Y-%m-%d %I:%M %p')})" task_listbox.insert(tk.END, task_str) task_listbox.itemconfig(tk.END, {'foreground': color}) main_task_indices.append(task_index) print("Listboxes updated with sorted tasks, colors, and checkmarks.") # Debug statement # Function to remove a task and its associated Google Calendar events def remove_task(): # Capture the selection index before any potential unhighlighting selected_task_index = task_listbox.curselection() selected_daily_task_index = daily_task_listbox.curselection() selected_weekly_task_index = weekly_task_listbox.curselection() selected_monthly_task_index = monthly_task_listbox.curselection() task_index_to_remove = None # Find which task is selected and map it to the original task index if selected_task_index: task_index_to_remove = main_task_indices[selected_task_index[0]] # Use the mapped main task index elif selected_daily_task_index: task_index_to_remove = daily_task_indices[selected_daily_task_index[0]] # Use the mapped daily task index elif selected_weekly_task_index: task_index_to_remove = weekly_task_indices[selected_weekly_task_index[0]] # Use the mapped weekly task index elif selected_monthly_task_index: task_index_to_remove = monthly_task_indices[selected_monthly_task_index[0]] # Use the mapped monthly task index # If no task was selected, show an error message if task_index_to_remove is None: messagebox.showwarning("Selection Error", "Please select a task to remove") return # Get the task to remove task_to_remove = tasks[task_index_to_remove] event_id = task_to_remove[16] # Assuming event_id is at index 16 # Remove the task from Google Calendar (if it has an associated event ID) if event_id: # Run deletion in a separate thread to ensure UI remains responsive threading.Thread(target=remove_task_from_google_calendar, args=(event_id,)).start() # Remove the task from the tasks list using the mapped index tasks.pop(task_index_to_remove) # Update the listbox UI and save tasks update_task_listbox() save_tasks() # Stop the alarm if the task being removed is currently causing the alarm to play global alarm_playing if alarm_playing: print(f"[DEBUG] Stopping alarm for removed task '{task_to_remove[0]}'") stop_alarm() # Stop the alarm sound # Clear the selection AFTER the task is removed task_listbox.selection_clear(0, tk.END) daily_task_listbox.selection_clear(0, tk.END) weekly_task_listbox.selection_clear(0, tk.END) monthly_task_listbox.selection_clear(0, tk.END) print(f"Task '{task_to_remove[0]}' removed successfully.") # Debugging message # Function to check if the user is logged in def is_logged_in(): return os.path.exists(TOKEN_FILE) # Perform login check when the program starts def check_login_status(): if is_logged_in(): google_login() # Automatically log in if token is found else: messagebox.showinfo("Login Required", "Please log in to Google Calendar") # Run the login check in a separate thread def check_login_status_in_background(): login_thread = threading.Thread(target=check_login_status) login_thread.start() # Function to clear selection of all listboxes def clear_all_selections(event): # Get the widget that was clicked widget = event.widget # List of widgets that should not trigger the clearing of selections excluded_widgets = [remove_task_button, add_task_button, mark_completed_button, mark_in_progress_button] # Check if the click was outside of the listboxes and excluded buttons if widget not in [task_listbox, daily_task_listbox, weekly_task_listbox, monthly_task_listbox] + excluded_widgets: task_listbox.selection_clear(0, tk.END) daily_task_listbox.selection_clear(0, tk.END) weekly_task_listbox.selection_clear(0, tk.END) monthly_task_listbox.selection_clear(0, tk.END) # Function to reset daily, weekly, and monthly tasks, even if in-progress def reset_tasks(): for i, task_data in enumerate(tasks): # Unpack task data (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed, in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data # Reset the completed and in_progress flags for all recurring tasks if daily or weekly or monthly: tasks[i] = (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, False, False, complete_at_datetime, complete_by_datetime, event_id) print(f"Recurring task '{task}' has been reset.") save_tasks() # Save the updated tasks after resetting update_task_listbox() # Update the UI # Function to check for tasks that need to alarm at the specified time and reset tasks at midnight def check_task_times(): last_checked_day = datetime.now().day while True: now = datetime.now() current_time = now.time().replace(second=0, microsecond=0) current_day = now.day print(f"[DEBUG] Current time: {current_time}") # Debugging: Print current time # Reset tasks at midnight or at the start of the next day if current_day != last_checked_day: print("[DEBUG] Midnight passed, resetting tasks...") reset_tasks() last_checked_day = current_day # Check task alarms for i, task_data in enumerate(tasks): if len(task_data) == 17: # Ensure task data has the expected structure (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly, daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed, in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data # Print debug information about each task print(f"[DEBUG] Checking task: {task}, In Progress: {in_progress}, Completed: {completed}") # "Complete at" task check if not in_progress and not completed and complete_at_datetime: complete_at_date_check = complete_at_datetime.date() complete_at_time_check = complete_at_datetime.time().replace(second=0, microsecond=0) # Compare both the date and the time if now.date() == complete_at_date_check and current_time == complete_at_time_check: print(f"[DEBUG] Complete at alarm triggered for: {task}") trigger_alarm(task, "Complete at") # "Complete by" task check if not in_progress and not completed and complete_by_datetime: complete_by_date_check = complete_by_datetime.date() complete_by_time_check = complete_by_datetime.time().replace(second=0, microsecond=0) # Compare both the date and the time if now.date() == complete_by_date_check and current_time == complete_by_time_check: print(f"[DEBUG] Complete by alarm triggered for: {task}") trigger_alarm(task, "Complete by") # Daily recurrence check if not in_progress and not completed and daily and daily_time: daily_time_check = daily_time.replace(second=0, microsecond=0) if current_time == daily_time_check: print(f"[DEBUG] Daily alarm triggered for: {task}") trigger_alarm(task, "Daily") # Weekly recurrence check if not in_progress and not completed and weekly and now.strftime("%A") == weekly_day and weekly_time: weekly_time_check = weekly_time.replace(second=0, microsecond=0) if current_time == weekly_time_check: print(f"[DEBUG] Weekly alarm triggered for: {task}") trigger_alarm(task, "Weekly") # Monthly recurrence check if not in_progress and not completed and monthly and now.day == int(monthly_day) and monthly_time: monthly_time_check = monthly_time.replace(second=0, microsecond=0) if current_time == monthly_time_check: print(f"[DEBUG] Monthly alarm triggered for: {task}") trigger_alarm(task, "Monthly") # Sleep for a short duration to avoid constant polling t.sleep(30) # Check every 30 seconds # Function to start the background thread to check task times def start_time_checker(): time_checker_thread = threading.Thread(target=check_task_times, daemon=True) time_checker_thread.start() # Function to manage the layout better by grouping inputs in a Frame def create_time_selection_frame(parent, hour_var, minute_var, am_pm_var, is_at): """Create a frame with time selection widgets (hour, minute, AM/PM) for compact layout.""" frame = tk.Frame(parent) hour_combo = ttk.Combobox(frame, values=[str(i) for i in range(1, 13)], width=3) hour_combo.set(hour_var) hour_combo.grid(row=0, column=0, padx=(0, 1), pady=0) minute_combo = ttk.Combobox(frame, values=[f"{i:02}" for i in range(0, 60)], width=3) minute_combo.set(minute_var) minute_combo.grid(row=0, column=1, padx=(1, 1), pady=0) am_pm_combo = ttk.Combobox(frame, values=["AM", "PM"], width=3) am_pm_combo.set(am_pm_var) am_pm_combo.grid(row=0, column=2, padx=(1, 0), pady=0) if is_at: global at_hour_combo, at_minute_combo, at_am_pm_combo at_hour_combo, at_minute_combo, at_am_pm_combo = hour_combo, minute_combo, am_pm_combo else: global by_hour_combo, by_minute_combo, by_am_pm_combo by_hour_combo, by_minute_combo, by_am_pm_combo = hour_combo, minute_combo, am_pm_combo return frame def create_date_selection_frame(parent): """Create a frame with month, day, and year selection dropdowns for date.""" frame = tk.Frame(parent) # Month dropdown month_combo = ttk.Combobox(frame, values=[f"{i:02}" for i in range(1, 13)], width=3) month_combo.set(datetime.now().strftime('%m')) month_combo.grid(row=0, column=0, padx=(0, 1), pady=0) # Day dropdown day_combo = ttk.Combobox(frame, values=[f"{i:02}" for i in range(1, 32)], width=3) day_combo.set(datetime.now().strftime('%d')) day_combo.grid(row=0, column=1, padx=(1, 1), pady=0) # Year dropdown current_year = datetime.now().year year_combo = ttk.Combobox(frame, values=[str(i) for i in range(current_year, current_year + 5)], width=5) year_combo.set(str(current_year)) year_combo.grid(row=0, column=2, padx=(1, 0), pady=0) return frame, month_combo, day_combo, year_combo # Function to toggle the visibility of the date selection frame def toggle_date_selection(at_checked, date_frame): if at_checked.get(): date_frame.grid() # Show the date frame else: date_frame.grid_remove() # Hide the date frame available_styles = root.style.theme_names() print(available_styles) # Create UI Elements # Define variables for "Complete at" and "Complete by" at_radio_var = ttk.IntVar() by_radio_var = ttk.IntVar() at_date_var = ttk.IntVar() by_date_var = ttk.IntVar() # Adjust the grid configuration for proper alignment and centering root.grid_columnconfigure(0, weight=1) root.grid_columnconfigure(1, weight=1) root.grid_columnconfigure(2, weight=1) root.grid_columnconfigure(3, weight=1) # Create a central Frame to hold both the time and date components for "Complete at" and "Complete by" login_button = ttk.Button(root, text="Login to Google", command=google_login, bootstyle="primary") login_button.grid(row=1, column=0, padx=20, pady=(10, 10), sticky="w") # Task entry frame task_notes_frame = ttk.Frame(root) task_notes_frame.grid(row=1, column=1, columnspan=2, padx=10, pady=(10, 10), sticky="ew") task_label = ttk.Label(task_notes_frame, text="Task:", bootstyle="info") task_label.grid(row=0, column=0, padx=(10, 0), pady=(10, 0), sticky="e") task_entry = ttk.Entry(task_notes_frame, width=50) task_entry.grid(row=0, column=1, padx=(5, 10), pady=(10, 0), sticky="ew") notes_label = ttk.Label(task_notes_frame, text="Notes:", bootstyle="info") notes_label.grid(row=1, column=0, padx=(10, 0), pady=(5, 0), sticky="ne") notes_entry = scrolledtext.ScrolledText(task_notes_frame, height=5, width=50) notes_entry.grid(row=1, column=1, padx=(5, 10), pady=(5, 5), sticky="ew") # Create the at_date_frame and by_date_frame globally global at_date_frame, by_date_frame # Create the central_frame (Complete at/by section) central_frame = ttk.Frame(root) central_frame.grid(row=2, column=1, columnspan=2, padx=10, pady=10, sticky="n") # "Complete at" date selection at_date_frame, at_month_combo, at_day_combo, at_year_combo = create_date_selection_frame(central_frame) at_date_frame.grid(row=0, column=4, padx=(5, 5), pady=0, sticky="w") at_date_frame.grid_remove() # Initially hidden until the checkbox is selected # "Complete by" date selection by_date_frame, by_month_combo, by_day_combo, by_year_combo = create_date_selection_frame(central_frame) by_date_frame.grid(row=1, column=4, padx=(5, 5), pady=0, sticky="w") by_date_frame.grid_remove() # Initially hidden until the checkbox is selected # "Complete at" time selection and date at_radio_button = ttk.Checkbutton(central_frame, text="Complete at", variable=at_radio_var, command=update_state, bootstyle="primary-round-toggle") at_radio_button.grid(row=0, column=0, padx=(5, 5), pady=0, sticky="w") at_time_frame = create_time_selection_frame(central_frame, "12", "00", "AM", is_at=True) at_time_frame.grid(row=0, column=1, padx=(5, 5), pady=0, sticky="w") at_date_checkbox = ttk.Checkbutton(central_frame, text="Add Date", variable=at_date_var, command=lambda: toggle_date_selection(at_date_var, at_date_frame), bootstyle="primary-round-toggle") at_date_checkbox.grid(row=0, column=3, padx=(5, 5), pady=0, sticky="w") # "Complete by" time selection and date by_radio_button = ttk.Checkbutton(central_frame, text="Complete by", variable=by_radio_var, command=update_state, bootstyle="primary-round-toggle") by_radio_button.grid(row=1, column=0, padx=(5, 5), pady=0, sticky="w") by_time_frame = create_time_selection_frame(central_frame, "12", "00", "AM", is_at=False) by_time_frame.grid(row=1, column=1, padx=(5, 5), pady=0, sticky="w") by_date_checkbox = ttk.Checkbutton(central_frame, text="Add Date", variable=by_date_var, command=lambda: toggle_date_selection(by_date_var, by_date_frame), bootstyle="primary-round-toggle") by_date_checkbox.grid(row=1, column=3, padx=(5, 5), pady=0, sticky="w") # Create the separator line between Complete at/by and Recurrence options separator = ttk.Separator(root, orient='horizontal') separator.grid(row=3, column=0, columnspan=4, sticky="ew", padx=10, pady=10) # Recurrence checkboxes and day selectors daily_var = ttk.IntVar() weekly_var = ttk.IntVar() monthly_var = ttk.IntVar() # Frame for Recurrence settings recurrence_frame = ttk.Frame(root) recurrence_frame.grid(row=4, column=1, columnspan=2, padx=5, pady=5, sticky="n") # Daily recurrence daily_checkbox = ttk.Checkbutton(recurrence_frame, text="Daily", variable=daily_var, command=update_state, bootstyle="primary-round-toggle") daily_checkbox.grid(row=0, column=0, padx=5, pady=5, sticky="w") daily_hour_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 13)], width=3, bootstyle="superhero") daily_hour_combo.set("12") daily_hour_combo.grid(row=0, column=1, padx=(2, 2), pady=5, sticky="e") daily_minute_combo = ttk.Combobox(recurrence_frame, values=[f"{i:02}" for i in range(0, 60)], width=3) daily_minute_combo.set("00") daily_minute_combo.grid(row=0, column=2, padx=(2, 2), pady=5, sticky="w") daily_am_pm_combo = ttk.Combobox(recurrence_frame, values=["AM", "PM"], width=3) daily_am_pm_combo.set("AM") daily_am_pm_combo.grid(row=0, column=3, padx=(2, 5), pady=5, sticky="w") # Weekly recurrence weekly_checkbox = ttk.Checkbutton(recurrence_frame, text="Weekly", variable=weekly_var, command=update_state, bootstyle="primary-round-toggle") weekly_checkbox.grid(row=1, column=0, padx=5, pady=5, sticky="w") weekly_day_combo = ttk.Combobox(recurrence_frame, values=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], width=10) weekly_day_combo.set("Monday") weekly_day_combo.grid(row=1, column=1, padx=(2, 2), pady=5, sticky="w") weekly_hour_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 13)], width=3) weekly_hour_combo.set("12") weekly_hour_combo.grid(row=1, column=2, padx=(2, 2), pady=5, sticky="w") weekly_minute_combo = ttk.Combobox(recurrence_frame, values=[f"{i:02}" for i in range(0, 60)], width=3) weekly_minute_combo.set("00") weekly_minute_combo.grid(row=1, column=3, padx=(2, 2), pady=5, sticky="w") weekly_am_pm_combo = ttk.Combobox(recurrence_frame, values=["AM", "PM"], width=3) weekly_am_pm_combo.set("AM") weekly_am_pm_combo.grid(row=1, column=4, padx=(2, 5), pady=5, sticky="w") # Monthly recurrence monthly_checkbox = ttk.Checkbutton(recurrence_frame, text="Monthly", variable=monthly_var, command=update_state, bootstyle="primary-round-toggle") monthly_checkbox.grid(row=2, column=0, padx=5, pady=5, sticky="w") monthly_day_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 32)], width=5) monthly_day_combo.set("1") monthly_day_combo.grid(row=2, column=1, padx=(2, 2), pady=5, sticky="e") monthly_hour_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 13)], width=3) monthly_hour_combo.set("12") monthly_hour_combo.grid(row=2, column=2, padx=(2, 2), pady=5, sticky="w") monthly_minute_combo = ttk.Combobox(recurrence_frame, values=[f"{i:02}" for i in range(0, 60)], width=3) monthly_minute_combo.set("00") monthly_minute_combo.grid(row=2, column=3, padx=(2, 2), pady=5, sticky="w") monthly_am_pm_combo = ttk.Combobox(recurrence_frame, values=["AM", "PM"], width=3) monthly_am_pm_combo.set("AM") monthly_am_pm_combo.grid(row=2, column=4, padx=(2, 5), pady=5, sticky="w") # Task list frame with scrollbars task_frame = ttk.Frame(root) task_frame.grid(row=5, column=0, columnspan=4, padx=10, pady=10, sticky="nsew") # Labels for task types daily_label = ttk.Label(task_frame, text="Daily Tasks", bootstyle="info") daily_label.grid(row=0, column=0, padx=5, pady=5) weekly_label = ttk.Label(task_frame, text="Weekly Tasks", bootstyle="info") weekly_label.grid(row=0, column=1, padx=5, pady=5) monthly_label = ttk.Label(task_frame, text="Monthly Tasks", bootstyle="info") monthly_label.grid(row=0, column=2, padx=5, pady=5) main_label = ttk.Label(task_frame, text="Main Tasks (Complete at/by)", bootstyle="info") main_label.grid(row=0, column=3, padx=5, pady=5) # Create task listboxes daily_task_listbox = tk.Listbox(task_frame, height=25, width=70) daily_task_listbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew") daily_scrollbar = ttk.Scrollbar(task_frame, orient="vertical") daily_scrollbar.grid(row=1, column=0, sticky="nse") daily_task_listbox.config(yscrollcommand=daily_scrollbar.set) daily_scrollbar.config(command=daily_task_listbox.yview) weekly_task_listbox = tk.Listbox(task_frame, height=25, width=70) weekly_task_listbox.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") weekly_scrollbar = ttk.Scrollbar(task_frame, orient="vertical") weekly_scrollbar.grid(row=1, column=1, sticky="nse") weekly_task_listbox.config(yscrollcommand=weekly_scrollbar.set) weekly_scrollbar.config(command=weekly_task_listbox.yview) monthly_task_listbox = tk.Listbox(task_frame, height=25, width=70) monthly_task_listbox.grid(row=1, column=2, padx=5, pady=5, sticky="nsew") monthly_scrollbar = ttk.Scrollbar(task_frame, orient="vertical") monthly_scrollbar.grid(row=1, column=2, sticky="nse") monthly_task_listbox.config(yscrollcommand=monthly_scrollbar.set) monthly_scrollbar.config(command=monthly_task_listbox.yview) task_listbox = tk.Listbox(task_frame, height=25, width=90) task_listbox.grid(row=1, column=3, padx=5, pady=5, sticky="nsew") main_scrollbar = ttk.Scrollbar(task_frame, orient="vertical") main_scrollbar.grid(row=1, column=3, sticky="nse") task_listbox.config(yscrollcommand=main_scrollbar.set) main_scrollbar.config(command=task_listbox.yview) # Button to add tasks button_frame = ttk.Frame(root) button_frame.grid(row=6, column=0, columnspan=4) add_task_button = ttk.Button(button_frame, text="Add Task", command=add_task, bootstyle="success") add_task_button.grid(row=0, column=0, padx=5, pady=10) mark_in_progress_button = ttk.Button(button_frame, text="Mark In-Progress", command=mark_task_in_progress, bootstyle="warning") mark_in_progress_button.grid(row=0, column=1, padx=5, pady=10) mark_completed_button = ttk.Button(button_frame, text="Mark Completed", command=mark_task_completed, bootstyle="succcess") mark_completed_button.grid(row=0, column=2, padx=5, pady=10) remove_task_button = ttk.Button(button_frame, text="Remove Task", command=remove_task, bootstyle="danger") remove_task_button.grid(row=0, column=3, padx=5, pady=10) # Bind double-click event to listboxes to show task notes task_listbox.bind("", show_notes) daily_task_listbox.bind("", show_notes) weekly_task_listbox.bind("", show_notes) monthly_task_listbox.bind("", show_notes) # Bind the click event on the root window to clear task selection root.bind("", clear_all_selections) # Allow natural selection behavior in listboxes task_listbox.bind("", lambda e: task_listbox.selection_anchor(task_listbox.nearest(e.y))) daily_task_listbox.bind("", lambda e: daily_task_listbox.selection_anchor(daily_task_listbox.nearest(e.y))) weekly_task_listbox.bind("", lambda e: weekly_task_listbox.selection_anchor(weekly_task_listbox.nearest(e.y))) monthly_task_listbox.bind("", lambda e: monthly_task_listbox.selection_anchor(monthly_task_listbox.nearest(e.y))) # Call this function right after creating all the widgets to set the initial state initialize_state() # Call the login check on startup in a separate thread check_login_status_in_background() # Load tasks on startup load_tasks() # Start the Tkinter event loop start_time_checker() root.mainloop()