Advertisement
thewindmage420

Task Tracker

Jan 19th, 2025
32
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 62.99 KB | None | 0 0
  1. import tkinter as tk
  2. from tkinter import ttk, messagebox
  3. import ttkbootstrap as ttk
  4. from ttkbootstrap.constants import *
  5. from tkinter import scrolledtext
  6. import pytz
  7. from datetime import datetime, timedelta, time
  8. import threading
  9. import time as t
  10. import pygame
  11. import json
  12. import os
  13. import sys
  14. from ctypes import windll
  15. from google.oauth2.credentials import Credentials
  16. from google_auth_oauthlib.flow import InstalledAppFlow
  17. from googleapiclient.discovery import build
  18. from google.auth.transport.requests import Request
  19.  
  20. # Initialize pygame mixer for sound playing
  21. pygame.mixer.init()
  22.  
  23. # Google Calendar API scope and token file
  24. SCOPES = ['https://www.googleapis.com/auth/calendar']
  25. TOKEN_FILE = 'token.json'
  26.  
  27. # Initialize the main window with a modern theme
  28. root = ttk.Window(themename="superhero")  # Change theme if needed
  29. root.title("Task Tracker")
  30. root.geometry("1500x950")
  31.  
  32. # Ensure the window appears in the taskbar and remove the default window frame
  33. root.attributes('-fullscreen', True)  # Not full screen, but this helps manage taskbar appearance
  34.  
  35. # Function to minimize the window
  36. def minimize_window():
  37.     root.iconify()
  38.  
  39. # Function to maximize or restore the window size
  40. is_maximized = False
  41. def toggle_maximize_restore():
  42.     global is_maximized
  43.     if not is_maximized:
  44.         root.state('zoomed')  # Maximize window
  45.         is_maximized = True
  46.     else:
  47.         root.state('normal')  # Restore to original size
  48.         is_maximized = False
  49.  
  50. # Function to close the window
  51. def close_window():
  52.     root.quit()
  53.  
  54. # Create a custom title bar frame at the top
  55. title_bar = ttk.Frame(root, bootstyle="primary", relief='raised', height=30)
  56. title_bar.grid(row=0, column=0, columnspan=4, sticky="ew")
  57.  
  58. # Add a title label
  59. title_label = ttk.Label(title_bar, text="Task Tracker", bootstyle="inverse-primary")
  60. title_label.pack(side="left", padx=10)
  61.  
  62. # Add minimize, maximize, and close buttons in the correct order
  63. close_button = ttk.Button(title_bar, text="X", command=close_window, bootstyle="danger-outline")
  64. close_button.pack(side="right", padx=5, pady=2)
  65.  
  66. minimize_button = ttk.Button(title_bar, text="_", command=minimize_window, bootstyle="warning-outline")
  67. minimize_button.pack(side="right", padx=5, pady=2)
  68.  
  69. # Variables to track window movement
  70. x_offset = 0
  71. y_offset = 0
  72.  
  73. # Functions to move the window
  74. def get_window_position(event):
  75.     global x_offset, y_offset
  76.     x_offset = event.x
  77.     y_offset = event.y
  78.  
  79. def move_window(event):
  80.     root.geometry(f'+{event.x_root - x_offset}+{event.y_root - y_offset}')
  81.  
  82. # Bind the title bar to allow dragging the window
  83. title_bar.bind("<Button-1>", get_window_position)
  84. title_bar.bind("<B1-Motion>", move_window)
  85.  
  86.  
  87. # Function to create a rounded rectangle in a canvas
  88. def round_rectangle(canvas, x1, y1, x2, y2, radius=25, **kwargs):
  89.     points = [x1 + radius, y1,
  90.               x1 + radius, y1,
  91.               x2 - radius, y1,
  92.               x2 - radius, y1,
  93.               x2, y1,
  94.               x2, y1 + radius,
  95.               x2, y1 + radius,
  96.               x2, y2 - radius,
  97.               x2, y2 - radius,
  98.               x2, y2,
  99.               x2 - radius, y2,
  100.               x2 - radius, y2,
  101.               x1 + radius, y2,
  102.               x1 + radius, y2,
  103.               x1, y2,
  104.               x1, y2 - radius,
  105.               x1, y2 - radius,
  106.               x1, y1 + radius,
  107.               x1, y1 + radius,
  108.               x1, y1]
  109.     return canvas.create_polygon(points, **kwargs, smooth=True)
  110.  
  111. # Create a canvas and use grid to place it in the window
  112. canvas = tk.Canvas(root, width=300, height=300, bg="blue", bd=0, highlightthickness=0)
  113. canvas.grid(row=1, column=0, sticky="nsew")
  114.  
  115. # Configure the grid to allow the canvas to expand and fill the window
  116. root.grid_rowconfigure(1, weight=1)
  117. root.grid_columnconfigure(0, weight=1)
  118.  
  119. # Define a list to store tasks and their completion status
  120. tasks = []
  121. alarm_playing = False  # Global flag to stop alarm
  122.  
  123. # Path to the task file
  124. TASK_FILE = "tasks.json"
  125.  
  126. # Path to the alarm sound file (ensure you have a sound file like "alarm.wav" in the directory)
  127. ALARM_SOUND_FILE = "alarm.wav"
  128.  
  129. # Global variable to store Google Calendar API service
  130. google_service = None
  131.  
  132. # Global dictionary to store event IDs from Google Calendar
  133. task_event_ids = {}
  134.  
  135. def play_alarm_sound():
  136.     global alarm_playing
  137.     if not alarm_playing:
  138.         try:
  139.             print("Playing alarm sound on a new thread...")
  140.             threading.Thread(target=_play_alarm_sound_worker).start()
  141.         except Exception as e:
  142.             print(f"Error starting sound thread: {e}")
  143.  
  144. def _play_alarm_sound_worker():
  145.     global alarm_playing
  146.     try:
  147.         pygame.mixer.music.load(ALARM_SOUND_FILE)
  148.         pygame.mixer.music.play(-1)  # Loop the sound indefinitely
  149.         alarm_playing = True
  150.     except Exception as e:
  151.         print(f"Error playing sound: {e}")
  152.            
  153. # Function to stop the alarm sound
  154. def stop_alarm():
  155.     global alarm_playing
  156.     if alarm_playing:
  157.         print("Stopping alarm...")
  158.         pygame.mixer.music.stop()  # Stop the sound
  159.         alarm_playing = False
  160.    
  161. # Function to save tasks to a JSON file (including in-progress flag)
  162. def save_tasks():
  163.     try:
  164.         with open(TASK_FILE, "w") as f:
  165.             # Convert datetime objects to ISO format strings for JSON serialization
  166.             tasks_serializable = []
  167.             for task in tasks:
  168.                 task_list = list(task)
  169.                 # Convert datetime objects to strings
  170.                 if task_list[14]:
  171.                     task_list[14] = task_list[14].isoformat()
  172.                 if task_list[15]:
  173.                     task_list[15] = task_list[15].isoformat()
  174.                 # Convert time objects to strings
  175.                 if task_list[2]:
  176.                     task_list[2] = task_list[2].isoformat()
  177.                 if task_list[3]:
  178.                     task_list[3] = task_list[3].isoformat()
  179.                 if task_list[7]:
  180.                     task_list[7] = task_list[7].isoformat()
  181.                 if task_list[9]:
  182.                     task_list[9] = task_list[9].isoformat()
  183.                 if task_list[11]:
  184.                     task_list[11] = task_list[11].isoformat()
  185.                 # event_id does not need conversion, but ensure it's included
  186.                 tasks_serializable.append(task_list)
  187.             json.dump(tasks_serializable, f)
  188.     except Exception as e:
  189.         print(f"Error saving tasks: {e}")
  190.  
  191. # Function to load tasks from a JSON file (including in-progress flag)
  192. def load_tasks():
  193.     global tasks
  194.     if os.path.exists(TASK_FILE):
  195.         try:
  196.             with open(TASK_FILE, "r") as f:
  197.                 loaded_tasks = json.load(f)
  198.                 tasks = []
  199.                 for task in loaded_tasks:
  200.                     task_data = (
  201.                         task[0],  # Task name
  202.                         task[1],  # Notes
  203.                         time.fromisoformat(task[2]) if task[2] else None,  # Complete at time
  204.                         time.fromisoformat(task[3]) if task[3] else None,  # Complete by time
  205.                         task[4],  # Daily
  206.                         task[5],  # Weekly
  207.                         task[6],  # Monthly
  208.                         time.fromisoformat(task[7]) if task[7] else None,  # Daily time
  209.                         task[8],  # Weekly day
  210.                         time.fromisoformat(task[9]) if task[9] else None,  # Weekly time
  211.                         task[10],  # Monthly day
  212.                         time.fromisoformat(task[11]) if task[11] else None,  # Monthly time
  213.                         task[12],  # Completed
  214.                         task[13],  # In-progress
  215.                         datetime.fromisoformat(task[14]) if task[14] else None,  # Complete at datetime
  216.                         datetime.fromisoformat(task[15]) if task[15] else None,  # Complete by datetime
  217.                         task[16] if len(task) > 16 else None  # event_id (Handle backward compatibility)
  218.                     )
  219.                     tasks.append(task_data)
  220.                 update_task_listbox()  # Load tasks into the listboxes
  221.         except Exception as e:
  222.             print(f"Error loading tasks: {e}")
  223.  
  224. # Function to show notification after alarm sound
  225. def show_alarm_popup(task, time_type):
  226.     messagebox.showinfo("Task Alarm", f"It's time to {time_type} for task: {task}")
  227.  
  228. # Function to trigger alarm (plays sound first, then popup)
  229. def trigger_alarm(task, time_type):
  230.     global alarm_playing
  231.  
  232.     # If the alarm is not already playing, start playing it in a new thread
  233.     if not alarm_playing:
  234.         print(f"Triggering alarm for task '{task}' at {time_type}")
  235.         play_alarm_sound()  # This will run in a separate thread
  236.  
  237.     # Show the task alarm popup using Tkinter's event loop
  238.     root.after(0, lambda: show_alarm_popup(task, time_type))
  239.  
  240. # Function to convert hour, minute, AM/PM to a time object
  241. def get_time_from_dropdown(hour, minute, am_pm):
  242.     hour = int(hour)
  243.     minute = int(minute)
  244.     if am_pm == "PM" and hour != 12:
  245.         hour += 12
  246.     if am_pm == "AM" and hour == 12:
  247.         hour = 0
  248.     return time(hour, minute)
  249.  
  250. # Function to enable/disable options based on selection
  251. def update_state():
  252.     # Reset all controls (enable all checkboxes and disable all time fields by default)
  253.     daily_checkbox.config(state=tk.NORMAL)
  254.     weekly_checkbox.config(state=tk.NORMAL)
  255.     monthly_checkbox.config(state=tk.NORMAL)
  256.     at_radio_button.config(state=tk.NORMAL)
  257.     by_radio_button.config(state=tk.NORMAL)
  258.  
  259.     at_date_checkbox.config(state=tk.DISABLED)
  260.     by_date_checkbox.config(state=tk.DISABLED)
  261.  
  262.     # Disable time selectors initially
  263.     daily_hour_combo.config(state=tk.DISABLED)
  264.     daily_minute_combo.config(state=tk.DISABLED)
  265.     daily_am_pm_combo.config(state=tk.DISABLED)
  266.  
  267.     weekly_day_combo.config(state=tk.DISABLED)
  268.     weekly_hour_combo.config(state=tk.DISABLED)
  269.     weekly_minute_combo.config(state=tk.DISABLED)
  270.     weekly_am_pm_combo.config(state=tk.DISABLED)
  271.  
  272.     monthly_day_combo.config(state=tk.DISABLED)
  273.     monthly_hour_combo.config(state=tk.DISABLED)
  274.     monthly_minute_combo.config(state=tk.DISABLED)
  275.     monthly_am_pm_combo.config(state=tk.DISABLED)
  276.  
  277.     at_hour_combo.config(state=tk.DISABLED)
  278.     at_minute_combo.config(state=tk.DISABLED)
  279.     at_am_pm_combo.config(state=tk.DISABLED)
  280.  
  281.     by_hour_combo.config(state=tk.DISABLED)
  282.     by_minute_combo.config(state=tk.DISABLED)
  283.     by_am_pm_combo.config(state=tk.DISABLED)
  284.  
  285.     # Handle mutually exclusive behavior between Daily, Weekly, Monthly, Complete at, and Complete by
  286.  
  287.     # If Daily is selected
  288.     if daily_var.get():
  289.         # Disable Weekly, Monthly, Complete at, and Complete by
  290.         weekly_checkbox.config(state=tk.DISABLED)
  291.         monthly_checkbox.config(state=tk.DISABLED)
  292.         at_radio_button.config(state=tk.DISABLED)
  293.         by_radio_button.config(state=tk.DISABLED)
  294.  
  295.         # Enable Daily time selectors
  296.         daily_hour_combo.config(state=tk.NORMAL)
  297.         daily_minute_combo.config(state=tk.NORMAL)
  298.         daily_am_pm_combo.config(state=tk.NORMAL)
  299.  
  300.     # If Weekly is selected
  301.     elif weekly_var.get():
  302.         # Disable Daily, Monthly, Complete at, and Complete by
  303.         daily_checkbox.config(state=tk.DISABLED)
  304.         monthly_checkbox.config(state=tk.DISABLED)
  305.         at_radio_button.config(state=tk.DISABLED)
  306.         by_radio_button.config(state=tk.DISABLED)
  307.  
  308.         # Enable Weekly time and day selectors
  309.         weekly_day_combo.config(state=tk.NORMAL)
  310.         weekly_hour_combo.config(state=tk.NORMAL)
  311.         weekly_minute_combo.config(state=tk.NORMAL)
  312.         weekly_am_pm_combo.config(state=tk.NORMAL)
  313.  
  314.     # If Monthly is selected
  315.     elif monthly_var.get():
  316.         # Disable Daily, Weekly, Complete at, and Complete by
  317.         daily_checkbox.config(state=tk.DISABLED)
  318.         weekly_checkbox.config(state=tk.DISABLED)
  319.         at_radio_button.config(state=tk.DISABLED)
  320.         by_radio_button.config(state=tk.DISABLED)
  321.  
  322.         # Enable Monthly day and time selectors
  323.         monthly_day_combo.config(state=tk.NORMAL)
  324.         monthly_hour_combo.config(state=tk.NORMAL)
  325.         monthly_minute_combo.config(state=tk.NORMAL)
  326.         monthly_am_pm_combo.config(state=tk.NORMAL)
  327.  
  328.     # If Complete at is selected (it does not disable Complete by)
  329.     if at_radio_var.get():
  330.         # Disable Daily, Weekly, and Monthly
  331.         daily_checkbox.config(state=tk.DISABLED)
  332.         weekly_checkbox.config(state=tk.DISABLED)
  333.         monthly_checkbox.config(state=tk.DISABLED)
  334.  
  335.         # Enable Complete at time selectors and Add Date checkbox
  336.         at_hour_combo.config(state=tk.NORMAL)
  337.         at_minute_combo.config(state=tk.NORMAL)
  338.         at_am_pm_combo.config(state=tk.NORMAL)
  339.         at_date_checkbox.config(state=tk.NORMAL)
  340.  
  341.         # If "Add Date" for Complete at is checked, show the date selectors
  342.         if at_date_var.get():
  343.             at_date_frame.grid()
  344.         else:
  345.             at_date_frame.grid_remove()
  346.  
  347.     # If Complete by is selected (it does not disable Complete at)
  348.     if by_radio_var.get():
  349.         # Disable Daily, Weekly, and Monthly
  350.         daily_checkbox.config(state=tk.DISABLED)
  351.         weekly_checkbox.config(state=tk.DISABLED)
  352.         monthly_checkbox.config(state=tk.DISABLED)
  353.  
  354.         # Enable Complete by time selectors and Add Date checkbox
  355.         by_hour_combo.config(state=tk.NORMAL)
  356.         by_minute_combo.config(state=tk.NORMAL)
  357.         by_am_pm_combo.config(state=tk.NORMAL)
  358.         by_date_checkbox.config(state=tk.NORMAL)
  359.  
  360.         # If "Add Date" for Complete by is checked, show the date selectors
  361.         if by_date_var.get():
  362.             by_date_frame.grid()
  363.         else:
  364.             by_date_frame.grid_remove()
  365.  
  366.     # Ensure the date frames are hidden if neither "Add Date" checkbox is checked
  367.     if not at_date_var.get():
  368.         at_date_frame.grid_remove()
  369.     if not by_date_var.get():
  370.         by_date_frame.grid_remove()
  371.  
  372.     # If neither "Complete at" nor "Complete by" is selected, ensure both date checkboxes and frames are disabled
  373.     if not at_radio_var.get() and not by_radio_var.get():
  374.         at_date_checkbox.config(state=tk.DISABLED)
  375.         by_date_checkbox.config(state=tk.DISABLED)
  376.         at_date_frame.grid_remove()
  377.         by_date_frame.grid_remove()
  378.  
  379. # Initialize the states to make sure the correct controls are disabled on start
  380. def initialize_state():
  381.     # Disable the "Add Date" checkboxes and date frames initially
  382.     at_date_checkbox.config(state=tk.DISABLED)
  383.     by_date_checkbox.config(state=tk.DISABLED)
  384.  
  385.     # Disable time selectors initially
  386.     at_hour_combo.config(state=tk.DISABLED)
  387.     at_minute_combo.config(state=tk.DISABLED)
  388.     at_am_pm_combo.config(state=tk.DISABLED)
  389.     by_hour_combo.config(state=tk.DISABLED)
  390.     by_minute_combo.config(state=tk.DISABLED)
  391.     by_am_pm_combo.config(state=tk.DISABLED)
  392.  
  393.     # Disable recurrence time selectors initially
  394.     daily_hour_combo.config(state=tk.DISABLED)
  395.     daily_minute_combo.config(state=tk.DISABLED)
  396.     daily_am_pm_combo.config(state=tk.DISABLED)
  397.     weekly_day_combo.config(state=tk.DISABLED)
  398.     weekly_hour_combo.config(state=tk.DISABLED)
  399.     weekly_minute_combo.config(state=tk.DISABLED)
  400.     weekly_am_pm_combo.config(state=tk.DISABLED)
  401.     monthly_day_combo.config(state=tk.DISABLED)
  402.     monthly_hour_combo.config(state=tk.DISABLED)
  403.     monthly_minute_combo.config(state=tk.DISABLED)
  404.     monthly_am_pm_combo.config(state=tk.DISABLED)
  405.  
  406. # Function to get the path to the credentials.json file
  407. def get_credentials_path():
  408.     """
  409.    Returns the correct path to the 'credentials.json' file depending on whether
  410.    the script is running as a standalone executable (PyInstaller) or as a Python script.
  411.    """
  412.     if hasattr(sys, '_MEIPASS'):
  413.         # If bundled with PyInstaller, look for the file in the _MEIPASS folder
  414.         return os.path.join(sys._MEIPASS, 'credentials.json')
  415.     else:
  416.         # When running normally as a script, use the current working directory
  417.         return os.path.join(os.getcwd(), 'credentials.json')
  418.  
  419. # Function to handle Google Login
  420. def google_login():
  421.     creds = None
  422.     SCOPES = ['https://www.googleapis.com/auth/calendar']
  423.  
  424.     credentials_path = get_credentials_path()  # Get the path to credentials.json
  425.  
  426.     if not os.path.exists(credentials_path):
  427.         # If credentials.json is missing, show an error message
  428.         root.after(0, lambda: messagebox.showerror("Credentials Missing", "credentials.json file not found in the program directory."))
  429.         return
  430.  
  431.     # Load credentials from the token.json file, if they exist
  432.     token_path = 'token.json'
  433.     if os.path.exists(token_path):
  434.         with open(token_path, 'r') as token_file:
  435.             creds = Credentials.from_authorized_user_info(json.load(token_file), SCOPES)
  436.  
  437.     # If there are no valid credentials, let the user log in
  438.     if not creds or not creds.valid:
  439.         if creds and creds.expired and creds.refresh_token:
  440.             try:
  441.                 creds.refresh(Request())  # Refresh credentials if they're expired
  442.             except Exception as e:
  443.                 root.after(0, lambda: messagebox.showerror("Login Error", f"Failed to refresh credentials: {e}"))
  444.                 return
  445.         else:
  446.             # If no credentials are available, prompt the user to log in
  447.             try:
  448.                 flow = InstalledAppFlow.from_client_secrets_file(credentials_path, SCOPES)
  449.                 creds = flow.run_local_server(port=0)
  450.             except Exception as e:
  451.                 root.after(0, lambda: messagebox.showerror("Login Error", f"Failed to authenticate using Google: {e}"))
  452.                 return
  453.  
  454.         # Save the credentials for the next run
  455.         with open(token_path, 'w') as token_file:
  456.             token_file.write(creds.to_json())
  457.  
  458.     # Now that we have valid credentials, build the Google Calendar service
  459.     try:
  460.         global google_service
  461.         google_service = build('calendar', 'v3', credentials=creds)
  462.         root.after(0, lambda: messagebox.showinfo("Login Success", "Successfully logged in to Google Calendar!"))
  463.     except Exception as e:
  464.         root.after(0, lambda: messagebox.showerror("Login Error", f"Failed to connect to Google Calendar: {e}"))
  465.  
  466. # Get the local time zone (you can change this to a specific time zone if needed)
  467. local_tz = pytz.timezone('America/New_York')  # Replace 'America/New_York' with your local time zone
  468.  
  469. # Function to add a task to Google Calendar with recurrence
  470. def add_task_to_google_calendar(task, start_time, notes=None, recurrence=None, duration_minutes=60):
  471.     """
  472.    Add a task to Google Calendar with a start time, optional recurrence, and duration.
  473.    """
  474.     if google_service is None:
  475.         messagebox.showerror("Not Logged In", "Please log in to Google Calendar first!")
  476.         return
  477.  
  478.     # Convert start_time to the user's local time zone
  479.     start_time = local_tz.localize(start_time)
  480.  
  481.     # Calculate end time by adding the duration to the start time (only if duration is provided)
  482.     if duration_minutes is not None:
  483.         end_time = start_time + timedelta(minutes=duration_minutes)
  484.     else:
  485.         end_time = start_time + timedelta(minutes=60)  # Default to 60 minutes if no duration is specified
  486.  
  487.     # Create the basic event structure
  488.     event = {
  489.         'summary': task,
  490.         'description': notes,
  491.         'start': {
  492.             'dateTime': start_time.isoformat(),  # Ensure time zone info is included
  493.             'timeZone': str(local_tz),  # Explicitly set the user's time zone
  494.         },
  495.         'end': {
  496.             'dateTime': end_time.isoformat(),  # Ensure time zone info is included
  497.             'timeZone': str(local_tz),  # Explicitly set the user's time zone
  498.         },
  499.         'reminders': {
  500.             'useDefault': False,
  501.             'overrides': [
  502.                 {'method': 'popup', 'minutes': 10},  # Popup reminder 10 minutes before the task
  503.             ],
  504.         }
  505.     }
  506.  
  507.     # Add recurrence if provided
  508.     if recurrence:
  509.         event['recurrence'] = recurrence
  510.  
  511.     try:
  512.         # Insert the event into Google Calendar
  513.         event = google_service.events().insert(calendarId='primary', body=event).execute()
  514.         print(f'Event created: {event.get("htmlLink")}')
  515.         return event['id']  # Return the event ID to store it
  516.     except Exception as e:
  517.         print(f"An error occurred: {e}")
  518.         messagebox.showerror("Error", f"Failed to add task to Google Calendar: {e}")
  519.         return None
  520.  
  521. # Function to remove task from Google Calendar
  522. def remove_task_from_google_calendar(event_id):
  523.     if google_service is None:
  524.         messagebox.showerror("Not Logged In", "Please log in to Google Calendar first!")
  525.         return
  526.  
  527.     try:
  528.         google_service.events().delete(calendarId='primary', eventId=event_id).execute()
  529.         print(f"Event with ID {event_id} deleted successfully from Google Calendar.")
  530.     except Exception as e:
  531.         print(f"An error occurred while deleting task: {e}")
  532.         messagebox.showerror("Error", f"Failed to delete task from Google Calendar: {e}")
  533.  
  534. # Function to convert hour, minute, AM/PM to a time object
  535. def get_time_from_dropdown(hour, minute, am_pm):
  536.     hour = int(hour)
  537.     minute = int(minute)
  538.     if am_pm == "PM" and hour != 12:
  539.         hour += 12
  540.     if am_pm == "AM" and hour == 12:
  541.         hour = 0
  542.     return time(hour, minute)
  543.  
  544. # Define checkboxes for date selection
  545. at_date_var = tk.IntVar()
  546. by_date_var = tk.IntVar()
  547.  
  548. # Function to add a task with a "Complete at" or "Complete by" time
  549. def add_task():
  550.     task = task_entry.get().strip()  # Get the task name from the entry field
  551.     notes = notes_entry.get("1.0", tk.END).strip()  # Get the notes from the Text widget
  552.     complete_at_selected = at_radio_var.get()
  553.     complete_by_selected = by_radio_var.get()
  554.  
  555.     complete_at_time = None
  556.     complete_by_time = None
  557.     start_time = None
  558.     end_time = None  # Used for "Complete by" and duration calculations
  559.     duration_minutes = 60  # Default duration
  560.  
  561.     complete_at_datetime = None
  562.     complete_by_datetime = None
  563.  
  564.     # Check for "Complete at" selection and retrieve the time
  565.     if complete_at_selected:
  566.         complete_at_time = get_time_from_dropdown(at_hour_combo.get(), at_minute_combo.get(), at_am_pm_combo.get())
  567.  
  568.         if at_date_var.get():  # If "Add Date" checkbox is checked
  569.             selected_date = datetime(
  570.                 int(at_year_combo.get()),
  571.                 int(at_month_combo.get()),
  572.                 int(at_day_combo.get())
  573.             ).date()
  574.         else:
  575.             selected_date = datetime.now().date()  # Default to today's date
  576.  
  577.         start_time = datetime.combine(selected_date, complete_at_time)
  578.         complete_at_datetime = start_time  # Store the datetime object
  579.  
  580.     # Check for "Complete by" selection and retrieve the time
  581.     if complete_by_selected:
  582.         complete_by_time = get_time_from_dropdown(by_hour_combo.get(), by_minute_combo.get(), by_am_pm_combo.get())
  583.  
  584.         if by_date_var.get():  # If "Add Date" checkbox is checked
  585.             selected_date = datetime(
  586.                 int(by_year_combo.get()),
  587.                 int(by_month_combo.get()),
  588.                 int(by_day_combo.get())
  589.             ).date()
  590.         else:
  591.             selected_date = datetime.now().date()  # Default to today's date
  592.  
  593.         end_time = datetime.combine(selected_date, complete_by_time)
  594.         complete_by_datetime = end_time  # Store the datetime object
  595.  
  596.         # Calculate duration if both Complete at and Complete by are selected
  597.         if complete_at_selected:
  598.             duration = end_time - start_time
  599.             duration_minutes = int(duration.total_seconds() / 60)  # Convert duration to minutes
  600.         else:
  601.             # If only Complete by is selected, set start_time based on default duration
  602.             start_time = end_time - timedelta(minutes=duration_minutes)
  603.  
  604.     # Handle recurrence (daily, weekly, monthly)
  605.     daily = daily_var.get()
  606.     weekly = weekly_var.get()
  607.     monthly = monthly_var.get()
  608.  
  609.     # Get time for recurrence options (if selected)
  610.     daily_time = get_time_from_dropdown(daily_hour_combo.get(), daily_minute_combo.get(), daily_am_pm_combo.get()) if daily else None
  611.     weekly_day = weekly_day_combo.get() if weekly else None
  612.     weekly_time = get_time_from_dropdown(weekly_hour_combo.get(), weekly_minute_combo.get(), weekly_am_pm_combo.get()) if weekly else None
  613.     monthly_day = monthly_day_combo.get() if monthly else None
  614.     monthly_time = get_time_from_dropdown(monthly_hour_combo.get(), monthly_minute_combo.get(), monthly_am_pm_combo.get()) if monthly else None
  615.  
  616.     event_id = None  # Initialize event_id
  617.  
  618.     if daily:
  619.         # Create a datetime object for daily tasks using the selected time for today
  620.         start_time = datetime.combine(datetime.now().date(), daily_time)
  621.  
  622.     if weekly:
  623.         # For weekly tasks, calculate the next occurrence of the selected day
  624.         day_mapping = {"Monday": "MO", "Tuesday": "TU", "Wednesday": "WE", "Thursday": "TH", "Friday": "FR", "Saturday": "SA", "Sunday": "SU"}
  625.         days_ahead = (["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].index(weekly_day) - datetime.now().weekday()) % 7
  626.         next_weekly_date = datetime.now() + timedelta(days=days_ahead)
  627.         # Combine the date of the next occurrence with the selected time
  628.         start_time = datetime.combine(next_weekly_date.date(), weekly_time)
  629.  
  630.     if monthly:
  631.         # For monthly tasks, calculate the next occurrence of the selected day of the month
  632.         today = datetime.now()
  633.         current_month = today.month
  634.         current_year = today.year
  635.  
  636.         # Adjust if the selected day is not valid for the month
  637.         try:
  638.             if today.day > int(monthly_day):
  639.                 if current_month == 12:  # Handle December to January rollover
  640.                     next_month_date = datetime(current_year + 1, 1, int(monthly_day))
  641.                 else:
  642.                     next_month_date = datetime(current_year, current_month + 1, int(monthly_day))
  643.             else:
  644.                 # If the day is in the future, schedule for the current month
  645.                 next_month_date = datetime(current_year, current_month, int(monthly_day))
  646.         except ValueError:
  647.             messagebox.showerror("Invalid Date", "The selected day is invalid for the current month.")
  648.             return
  649.  
  650.         # Combine the date of the selected day with the selected time
  651.         start_time = datetime.combine(next_month_date.date(), monthly_time)
  652.  
  653.     # Validate the task before adding it
  654.     if task:
  655.         # Add the task to Google Calendar
  656.         if google_service:
  657.             if daily:
  658.                 event_id = add_task_to_google_calendar(task, start_time, notes, recurrence=['RRULE:FREQ=DAILY'], duration_minutes=duration_minutes)
  659.             elif weekly:
  660.                 day_code = day_mapping[weekly_day]  # Get the correct day code for the recurrence rule
  661.                 event_id = add_task_to_google_calendar(task, start_time, notes, recurrence=[f'RRULE:FREQ=WEEKLY;BYDAY={day_code}'], duration_minutes=duration_minutes)
  662.             elif monthly:
  663.                 event_id = add_task_to_google_calendar(task, start_time, notes, recurrence=[f'RRULE:FREQ=MONTHLY;BYMONTHDAY={monthly_day}'], duration_minutes=duration_minutes)
  664.             elif complete_at_selected or complete_by_selected:
  665.                 event_id = add_task_to_google_calendar(task, start_time, notes, duration_minutes=duration_minutes)
  666.         else:
  667.             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?"):
  668.                 pass
  669.             else:
  670.                 return
  671.  
  672.         # Append the task to the tasks list, including the event_id
  673.         tasks.append((
  674.             task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  675.             daily_time, weekly_day, weekly_time, monthly_day, monthly_time, False, False,
  676.             complete_at_datetime, complete_by_datetime, event_id  # Include event_id here
  677.         ))
  678.         update_task_listbox()  # Update the UI
  679.         task_entry.delete(0, tk.END)  # Clear the task entry field
  680.         notes_entry.delete("1.0", tk.END)  # Clear the notes entry field
  681.         save_tasks()  # Save the task list
  682.     else:
  683.         messagebox.showwarning("Input Error", "Task cannot be empty!")
  684.        
  685. # Function to show the notes for the selected task in a separate window
  686. def show_notes(event):
  687.     listbox = event.widget
  688.     selected_index = listbox.curselection()
  689.  
  690.     if selected_index:
  691.         # Fetch the original index based on the listbox type (Main, Daily, Weekly, Monthly)
  692.         if listbox == task_listbox:
  693.             task_index = main_task_indices[selected_index[0]]
  694.         elif listbox == daily_task_listbox:
  695.             task_index = daily_task_indices[selected_index[0]]
  696.         elif listbox == weekly_task_listbox:
  697.             task_index = weekly_task_indices[selected_index[0]]
  698.         elif listbox == monthly_task_listbox:
  699.             task_index = monthly_task_indices[selected_index[0]]
  700.  
  701.         task_details = tasks[task_index]
  702.         notes = task_details[1]  # Get the notes associated with the task
  703.        
  704.         if notes.strip():  # Only show the window if there are notes
  705.             # Create a new window to show the notes
  706.             notes_window = tk.Toplevel(root)
  707.             notes_window.title(f"Notes for {task_details[0]}")
  708.            
  709.             # Enable word wrapping in the Text widget by setting wrap="word"
  710.             notes_text = tk.Text(notes_window, height=10, width=40, wrap="word")
  711.             notes_text.pack(padx=10, pady=10)
  712.            
  713.             notes_text.insert(tk.END, notes)
  714.             notes_text.config(state=tk.DISABLED)  # Make the notes read-only
  715.         else:
  716.             messagebox.showinfo("No Notes", f"There are no notes for the task: {task_details[0]}")
  717.  
  718. # Function to mark a task as in-progress and stop the alarm if it's playing
  719. def mark_task_in_progress():
  720.     selected_task_index = task_listbox.curselection()
  721.     selected_daily_task_index = daily_task_listbox.curselection()
  722.     selected_weekly_task_index = weekly_task_listbox.curselection()
  723.     selected_monthly_task_index = monthly_task_listbox.curselection()
  724.  
  725.     task_index = None
  726.  
  727.     # Check which listbox has a selected task
  728.     if selected_task_index:
  729.         task_index = main_task_indices[selected_task_index[0]]  # Use the mapped main task index
  730.     elif selected_daily_task_index:
  731.         task_index = daily_task_indices[selected_daily_task_index[0]]  # Use the mapped daily task index
  732.     elif selected_weekly_task_index:
  733.         task_index = weekly_task_indices[selected_weekly_task_index[0]]  # Use the mapped weekly task index
  734.     elif selected_monthly_task_index:
  735.         task_index = monthly_task_indices[selected_monthly_task_index[0]]  # Use the mapped monthly task index
  736.     else:
  737.         messagebox.showwarning("Selection Error", "Please select a task to mark as In-Progress")
  738.         return
  739.  
  740.     if task_index is not None:
  741.         task = tasks[task_index]
  742.         # Mark the task as in-progress (set `in_progress` to True, `completed` to False)
  743.         tasks[task_index] = (
  744.             task[0],  # Task name
  745.             task[1],  # Notes
  746.             task[2],  # Complete at time
  747.             task[3],  # Complete by time
  748.             task[4],  # Daily flag
  749.             task[5],  # Weekly flag
  750.             task[6],  # Monthly flag
  751.             task[7],  # Daily time
  752.             task[8],  # Weekly day
  753.             task[9],  # Weekly time
  754.             task[10], # Monthly day
  755.             task[11], # Monthly time
  756.             False,    # Completed flag set to False
  757.             True,     # In-progress flag set to True
  758.             task[14], # Complete at datetime
  759.             task[15], # Complete by datetime
  760.             task[16]  # event_id (Include this)
  761.         )
  762.  
  763.         # Stop the alarm if it is playing
  764.         stop_alarm()  # Ensure alarm stops immediately when a task is marked in-progress
  765.  
  766.         # Update the listboxes to reflect the change
  767.         update_task_listbox()
  768.         save_tasks()  # Save the updated tasks
  769.  
  770.         # Clear selection from all listboxes
  771.         task_listbox.selection_clear(0, tk.END)
  772.         daily_task_listbox.selection_clear(0, tk.END)
  773.         weekly_task_listbox.selection_clear(0, tk.END)
  774.         monthly_task_listbox.selection_clear(0, tk.END)
  775.  
  776.         print(f"Task '{task[0]}' marked as In-Progress.")  # Debugging message
  777.     else:
  778.         print("No task index found for In-Progress.")  # Debugging message
  779.  
  780. # Function to mark a task as completed and stop the alarm if it's playing
  781. def mark_task_completed():
  782.     selected_task_index = task_listbox.curselection()
  783.     selected_daily_task_index = daily_task_listbox.curselection()
  784.     selected_weekly_task_index = weekly_task_listbox.curselection()
  785.     selected_monthly_task_index = monthly_task_listbox.curselection()
  786.  
  787.     task_index = None
  788.  
  789.     # Check which listbox has a selected task
  790.     if selected_task_index:
  791.         task_index = main_task_indices[selected_task_index[0]]  # Use the mapped main task index
  792.     elif selected_daily_task_index:
  793.         task_index = daily_task_indices[selected_daily_task_index[0]]  # Use the mapped daily task index
  794.     elif selected_weekly_task_index:
  795.         task_index = weekly_task_indices[selected_weekly_task_index[0]]  # Use the mapped weekly task index
  796.     elif selected_monthly_task_index:
  797.         task_index = monthly_task_indices[selected_monthly_task_index[0]]  # Use the mapped monthly task index
  798.     else:
  799.         messagebox.showwarning("Selection Error", "Please select a task to mark as completed")
  800.         return
  801.  
  802.     if task_index is not None:
  803.         task_data = tasks[task_index]
  804.         # Unpack task data
  805.         (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  806.          daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed,
  807.          in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data
  808.  
  809.         # Mark the task as completed (set `completed` to True and `in_progress` to False)
  810.         tasks[task_index] = (
  811.             task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  812.             daily_time, weekly_day, weekly_time, monthly_day, monthly_time, True, False,
  813.             complete_at_datetime, complete_by_datetime, event_id  # Repack with new status
  814.         )
  815.  
  816.         # Stop the alarm if it is playing
  817.         stop_alarm()  # Ensure alarm stops immediately when a task is marked completed
  818.  
  819.         # Update the listboxes to reflect the change (it will remain in the list with a green checkmark)
  820.         update_task_listbox()
  821.         save_tasks()  # Save the updated tasks
  822.  
  823.         print(f"Task '{task}' marked as completed.")  # Debugging message
  824.  
  825. # Initialize lists to map listbox selections to task indices
  826. daily_task_indices = []
  827. weekly_task_indices = []
  828. monthly_task_indices = []
  829. main_task_indices = []
  830.  
  831. # Function to update the task listbox with colors and checkmarks for completed tasks
  832. # Initialize lists to map listbox selections to task indices
  833. daily_task_indices = []
  834. weekly_task_indices = []
  835. monthly_task_indices = []
  836. main_task_indices = []
  837.  
  838. # Function to update the task listboxes with colors and checkmarks for completed tasks
  839. def update_task_listbox():
  840.     # Clear existing tasks from the listboxes
  841.     daily_task_listbox.delete(0, tk.END)
  842.     weekly_task_listbox.delete(0, tk.END)
  843.     monthly_task_listbox.delete(0, tk.END)
  844.     task_listbox.delete(0, tk.END)  # Clear the main task listbox
  845.    
  846.     # Clear the index mapping lists
  847.     daily_task_indices.clear()
  848.     weekly_task_indices.clear()
  849.     monthly_task_indices.clear()
  850.     main_task_indices.clear()
  851.  
  852.     # Define checkmark and colors
  853.     checkmark = '✓'
  854.     in_progress_marker = '▼'
  855.     incomplete_color = 'white'
  856.     complete_color = 'lightgreen'
  857.     in_progress_color = 'yellow'  # Dark goldenrod color
  858.  
  859.     # Function to get the sorting key for tasks
  860.     def get_sort_key(task_data):
  861.         (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  862.          daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed,
  863.          in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data
  864.  
  865.         now = datetime.now()
  866.  
  867.         if complete_at_datetime:
  868.             return complete_at_datetime
  869.         elif complete_by_datetime:
  870.             return complete_by_datetime
  871.         elif daily and daily_time:
  872.             return datetime.combine(now.date(), daily_time)
  873.         elif weekly and weekly_time:
  874.             # Calculate the next occurrence of the weekly task
  875.             days_ahead = (["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].index(weekly_day) - now.weekday()) % 7
  876.             next_weekly_date = now + timedelta(days=days_ahead)
  877.             return datetime.combine(next_weekly_date.date(), weekly_time)
  878.         elif monthly and monthly_time:
  879.             # Calculate the next occurrence of the monthly task
  880.             today = now
  881.             current_month = today.month
  882.             current_year = today.year
  883.             monthly_day_int = int(monthly_day)
  884.             try:
  885.                 if today.day > monthly_day_int:
  886.                     if current_month == 12:  # Handle December to January rollover
  887.                         next_month_date = datetime(current_year + 1, 1, monthly_day_int)
  888.                     else:
  889.                         next_month_date = datetime(current_year, current_month + 1, monthly_day_int)
  890.                 else:
  891.                     next_month_date = datetime(current_year, current_month, monthly_day_int)
  892.                 return datetime.combine(next_month_date.date(), monthly_time)
  893.             except ValueError:
  894.                 # If the day is invalid for the month (e.g., February 30), place it at the end
  895.                 return datetime.max
  896.         else:
  897.             return datetime.max  # Tasks without a specific time are placed at the end
  898.  
  899.     # Build a list of tasks with their original indices
  900.     tasks_with_indices = list(enumerate(tasks))
  901.  
  902.     # Sort tasks by the calculated sort key while keeping the original indices
  903.     tasks_sorted = sorted(tasks_with_indices, key=lambda x: get_sort_key(x[1]))
  904.  
  905.     # Loop through sorted tasks and populate the respective listboxes
  906.     for task_index, task_data in tasks_sorted:
  907.         if len(task_data) == 17:
  908.             (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  909.              daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed,
  910.              in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data
  911.         else:
  912.             continue  # Skip if the task data doesn't have the right format
  913.  
  914.         # Decide the symbol and color based on the status
  915.         if completed:
  916.             task_str = f"{checkmark} {task}"
  917.             color = complete_color
  918.         elif in_progress:
  919.             task_str = f"{in_progress_marker} {task}"
  920.             color = in_progress_color
  921.         else:
  922.             task_str = task
  923.             color = incomplete_color
  924.  
  925.         # Add task to the appropriate listbox
  926.         if daily:
  927.             task_str += f" [Daily at {daily_time.strftime('%I:%M %p')}]"
  928.             daily_task_listbox.insert(tk.END, task_str)
  929.             daily_task_listbox.itemconfig(tk.END, {'foreground': color})
  930.             daily_task_indices.append(task_index)
  931.         elif weekly:
  932.             task_str += f" [Weekly on {weekly_day} at {weekly_time.strftime('%I:%M %p')}]"
  933.             weekly_task_listbox.insert(tk.END, task_str)
  934.             weekly_task_listbox.itemconfig(tk.END, {'foreground': color})
  935.             weekly_task_indices.append(task_index)
  936.         elif monthly:
  937.             task_str += f" [Monthly on day {monthly_day} at {monthly_time.strftime('%I:%M %p')}]"
  938.             monthly_task_listbox.insert(tk.END, task_str)
  939.             monthly_task_listbox.itemconfig(tk.END, {'foreground': color})
  940.             monthly_task_indices.append(task_index)
  941.         else:
  942.             # For Main tasks (Complete at/by)
  943.             if complete_at_datetime:
  944.                 task_str += f" (Complete at: {complete_at_datetime.strftime('%Y-%m-%d %I:%M %p')})"
  945.             if complete_by_datetime:
  946.                 task_str += f" (Complete by: {complete_by_datetime.strftime('%Y-%m-%d %I:%M %p')})"
  947.             task_listbox.insert(tk.END, task_str)
  948.             task_listbox.itemconfig(tk.END, {'foreground': color})
  949.             main_task_indices.append(task_index)
  950.            
  951.     print("Listboxes updated with sorted tasks, colors, and checkmarks.")  # Debug statement
  952.  
  953. # Function to remove a task and its associated Google Calendar events
  954. def remove_task():
  955.     # Capture the selection index before any potential unhighlighting
  956.     selected_task_index = task_listbox.curselection()
  957.     selected_daily_task_index = daily_task_listbox.curselection()
  958.     selected_weekly_task_index = weekly_task_listbox.curselection()
  959.     selected_monthly_task_index = monthly_task_listbox.curselection()
  960.  
  961.     task_index_to_remove = None
  962.  
  963.     # Find which task is selected and map it to the original task index
  964.     if selected_task_index:
  965.         task_index_to_remove = main_task_indices[selected_task_index[0]]  # Use the mapped main task index
  966.     elif selected_daily_task_index:
  967.         task_index_to_remove = daily_task_indices[selected_daily_task_index[0]]  # Use the mapped daily task index
  968.     elif selected_weekly_task_index:
  969.         task_index_to_remove = weekly_task_indices[selected_weekly_task_index[0]]  # Use the mapped weekly task index
  970.     elif selected_monthly_task_index:
  971.         task_index_to_remove = monthly_task_indices[selected_monthly_task_index[0]]  # Use the mapped monthly task index
  972.  
  973.     # If no task was selected, show an error message
  974.     if task_index_to_remove is None:
  975.         messagebox.showwarning("Selection Error", "Please select a task to remove")
  976.         return
  977.  
  978.     # Get the task to remove
  979.     task_to_remove = tasks[task_index_to_remove]
  980.     event_id = task_to_remove[16]  # Assuming event_id is at index 16
  981.  
  982.     # Remove the task from Google Calendar (if it has an associated event ID)
  983.     if event_id:
  984.         # Run deletion in a separate thread to ensure UI remains responsive
  985.         threading.Thread(target=remove_task_from_google_calendar, args=(event_id,)).start()
  986.  
  987.     # Remove the task from the tasks list using the mapped index
  988.     tasks.pop(task_index_to_remove)
  989.  
  990.     # Update the listbox UI and save tasks
  991.     update_task_listbox()
  992.     save_tasks()
  993.  
  994.     # Stop the alarm if the task being removed is currently causing the alarm to play
  995.     global alarm_playing
  996.     if alarm_playing:
  997.         print(f"[DEBUG] Stopping alarm for removed task '{task_to_remove[0]}'")
  998.         stop_alarm()  # Stop the alarm sound
  999.  
  1000.     # Clear the selection AFTER the task is removed
  1001.     task_listbox.selection_clear(0, tk.END)
  1002.     daily_task_listbox.selection_clear(0, tk.END)
  1003.     weekly_task_listbox.selection_clear(0, tk.END)
  1004.     monthly_task_listbox.selection_clear(0, tk.END)
  1005.  
  1006.     print(f"Task '{task_to_remove[0]}' removed successfully.")  # Debugging message
  1007.  
  1008. # Function to check if the user is logged in
  1009. def is_logged_in():
  1010.     return os.path.exists(TOKEN_FILE)
  1011.  
  1012. # Perform login check when the program starts
  1013. def check_login_status():
  1014.     if is_logged_in():
  1015.         google_login()  # Automatically log in if token is found
  1016.     else:
  1017.         messagebox.showinfo("Login Required", "Please log in to Google Calendar")
  1018.  
  1019. # Run the login check in a separate thread
  1020. def check_login_status_in_background():
  1021.     login_thread = threading.Thread(target=check_login_status)
  1022.     login_thread.start()
  1023.  
  1024. # Function to clear selection of all listboxes
  1025. def clear_all_selections(event):
  1026.     # Get the widget that was clicked
  1027.     widget = event.widget
  1028.  
  1029.     # List of widgets that should not trigger the clearing of selections
  1030.     excluded_widgets = [remove_task_button, add_task_button, mark_completed_button, mark_in_progress_button]
  1031.  
  1032.     # Check if the click was outside of the listboxes and excluded buttons
  1033.     if widget not in [task_listbox, daily_task_listbox, weekly_task_listbox, monthly_task_listbox] + excluded_widgets:
  1034.         task_listbox.selection_clear(0, tk.END)
  1035.         daily_task_listbox.selection_clear(0, tk.END)
  1036.         weekly_task_listbox.selection_clear(0, tk.END)
  1037.         monthly_task_listbox.selection_clear(0, tk.END)
  1038.  
  1039. # Function to reset daily, weekly, and monthly tasks, even if in-progress
  1040. def reset_tasks():
  1041.     for i, task_data in enumerate(tasks):
  1042.         # Unpack task data
  1043.         (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  1044.          daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed,
  1045.          in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data
  1046.  
  1047.         # Reset the completed and in_progress flags for all recurring tasks
  1048.         if daily or weekly or monthly:
  1049.             tasks[i] = (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  1050.                         daily_time, weekly_day, weekly_time, monthly_day, monthly_time, False, False,
  1051.                         complete_at_datetime, complete_by_datetime, event_id)
  1052.             print(f"Recurring task '{task}' has been reset.")
  1053.  
  1054.     save_tasks()  # Save the updated tasks after resetting
  1055.     update_task_listbox()  # Update the UI
  1056.  
  1057. # Function to check for tasks that need to alarm at the specified time and reset tasks at midnight
  1058. def check_task_times():
  1059.     last_checked_day = datetime.now().day
  1060.     while True:
  1061.         now = datetime.now()
  1062.         current_time = now.time().replace(second=0, microsecond=0)
  1063.         current_day = now.day
  1064.  
  1065.         print(f"[DEBUG] Current time: {current_time}")  # Debugging: Print current time
  1066.  
  1067.         # Reset tasks at midnight or at the start of the next day
  1068.         if current_day != last_checked_day:
  1069.             print("[DEBUG] Midnight passed, resetting tasks...")
  1070.             reset_tasks()
  1071.             last_checked_day = current_day
  1072.  
  1073.         # Check task alarms
  1074.         for i, task_data in enumerate(tasks):
  1075.             if len(task_data) == 17:  # Ensure task data has the expected structure
  1076.                 (task, notes, complete_at_time, complete_by_time, daily, weekly, monthly,
  1077.                  daily_time, weekly_day, weekly_time, monthly_day, monthly_time, completed,
  1078.                  in_progress, complete_at_datetime, complete_by_datetime, event_id) = task_data
  1079.  
  1080.                 # Print debug information about each task
  1081.                 print(f"[DEBUG] Checking task: {task}, In Progress: {in_progress}, Completed: {completed}")
  1082.  
  1083.                 # "Complete at" task check
  1084.                 if not in_progress and not completed and complete_at_datetime:
  1085.                     complete_at_date_check = complete_at_datetime.date()
  1086.                     complete_at_time_check = complete_at_datetime.time().replace(second=0, microsecond=0)
  1087.                    
  1088.                     # Compare both the date and the time
  1089.                     if now.date() == complete_at_date_check and current_time == complete_at_time_check:
  1090.                         print(f"[DEBUG] Complete at alarm triggered for: {task}")
  1091.                         trigger_alarm(task, "Complete at")
  1092.  
  1093.                 # "Complete by" task check
  1094.                 if not in_progress and not completed and complete_by_datetime:
  1095.                     complete_by_date_check = complete_by_datetime.date()
  1096.                     complete_by_time_check = complete_by_datetime.time().replace(second=0, microsecond=0)
  1097.                    
  1098.                     # Compare both the date and the time
  1099.                     if now.date() == complete_by_date_check and current_time == complete_by_time_check:
  1100.                         print(f"[DEBUG] Complete by alarm triggered for: {task}")
  1101.                         trigger_alarm(task, "Complete by")
  1102.  
  1103.                 # Daily recurrence check
  1104.                 if not in_progress and not completed and daily and daily_time:
  1105.                     daily_time_check = daily_time.replace(second=0, microsecond=0)
  1106.                     if current_time == daily_time_check:
  1107.                         print(f"[DEBUG] Daily alarm triggered for: {task}")
  1108.                         trigger_alarm(task, "Daily")
  1109.  
  1110.                 # Weekly recurrence check
  1111.                 if not in_progress and not completed and weekly and now.strftime("%A") == weekly_day and weekly_time:
  1112.                     weekly_time_check = weekly_time.replace(second=0, microsecond=0)
  1113.                     if current_time == weekly_time_check:
  1114.                         print(f"[DEBUG] Weekly alarm triggered for: {task}")
  1115.                         trigger_alarm(task, "Weekly")
  1116.  
  1117.                 # Monthly recurrence check
  1118.                 if not in_progress and not completed and monthly and now.day == int(monthly_day) and monthly_time:
  1119.                     monthly_time_check = monthly_time.replace(second=0, microsecond=0)
  1120.                     if current_time == monthly_time_check:
  1121.                         print(f"[DEBUG] Monthly alarm triggered for: {task}")
  1122.                         trigger_alarm(task, "Monthly")
  1123.  
  1124.         # Sleep for a short duration to avoid constant polling
  1125.         t.sleep(30)  # Check every 30 seconds
  1126.  
  1127. # Function to start the background thread to check task times
  1128. def start_time_checker():
  1129.     time_checker_thread = threading.Thread(target=check_task_times, daemon=True)
  1130.     time_checker_thread.start()
  1131.  
  1132. # Function to manage the layout better by grouping inputs in a Frame
  1133. def create_time_selection_frame(parent, hour_var, minute_var, am_pm_var, is_at):
  1134.     """Create a frame with time selection widgets (hour, minute, AM/PM) for compact layout."""
  1135.     frame = tk.Frame(parent)
  1136.    
  1137.     hour_combo = ttk.Combobox(frame, values=[str(i) for i in range(1, 13)], width=3)
  1138.     hour_combo.set(hour_var)
  1139.     hour_combo.grid(row=0, column=0, padx=(0, 1), pady=0)
  1140.  
  1141.     minute_combo = ttk.Combobox(frame, values=[f"{i:02}" for i in range(0, 60)], width=3)
  1142.     minute_combo.set(minute_var)
  1143.     minute_combo.grid(row=0, column=1, padx=(1, 1), pady=0)
  1144.  
  1145.     am_pm_combo = ttk.Combobox(frame, values=["AM", "PM"], width=3)
  1146.     am_pm_combo.set(am_pm_var)
  1147.     am_pm_combo.grid(row=0, column=2, padx=(1, 0), pady=0)
  1148.  
  1149.     if is_at:
  1150.         global at_hour_combo, at_minute_combo, at_am_pm_combo
  1151.         at_hour_combo, at_minute_combo, at_am_pm_combo = hour_combo, minute_combo, am_pm_combo
  1152.     else:
  1153.         global by_hour_combo, by_minute_combo, by_am_pm_combo
  1154.         by_hour_combo, by_minute_combo, by_am_pm_combo = hour_combo, minute_combo, am_pm_combo
  1155.  
  1156.     return frame
  1157.  
  1158. def create_date_selection_frame(parent):
  1159.     """Create a frame with month, day, and year selection dropdowns for date."""
  1160.     frame = tk.Frame(parent)
  1161.    
  1162.     # Month dropdown
  1163.     month_combo = ttk.Combobox(frame, values=[f"{i:02}" for i in range(1, 13)], width=3)
  1164.     month_combo.set(datetime.now().strftime('%m'))
  1165.     month_combo.grid(row=0, column=0, padx=(0, 1), pady=0)
  1166.  
  1167.     # Day dropdown
  1168.     day_combo = ttk.Combobox(frame, values=[f"{i:02}" for i in range(1, 32)], width=3)
  1169.     day_combo.set(datetime.now().strftime('%d'))
  1170.     day_combo.grid(row=0, column=1, padx=(1, 1), pady=0)
  1171.  
  1172.     # Year dropdown
  1173.     current_year = datetime.now().year
  1174.     year_combo = ttk.Combobox(frame, values=[str(i) for i in range(current_year, current_year + 5)], width=5)
  1175.     year_combo.set(str(current_year))
  1176.     year_combo.grid(row=0, column=2, padx=(1, 0), pady=0)
  1177.  
  1178.     return frame, month_combo, day_combo, year_combo
  1179.  
  1180. # Function to toggle the visibility of the date selection frame
  1181. def toggle_date_selection(at_checked, date_frame):
  1182.     if at_checked.get():
  1183.         date_frame.grid()  # Show the date frame
  1184.     else:
  1185.         date_frame.grid_remove()  # Hide the date frame
  1186.  
  1187. available_styles = root.style.theme_names()
  1188. print(available_styles)
  1189.  
  1190. # Create UI Elements
  1191. # Define variables for "Complete at" and "Complete by"
  1192. at_radio_var = ttk.IntVar()
  1193. by_radio_var = ttk.IntVar()
  1194. at_date_var = ttk.IntVar()
  1195. by_date_var = ttk.IntVar()
  1196.  
  1197. # Adjust the grid configuration for proper alignment and centering
  1198. root.grid_columnconfigure(0, weight=1)
  1199. root.grid_columnconfigure(1, weight=1)
  1200. root.grid_columnconfigure(2, weight=1)
  1201. root.grid_columnconfigure(3, weight=1)
  1202.  
  1203. # Create a central Frame to hold both the time and date components for "Complete at" and "Complete by"
  1204. login_button = ttk.Button(root, text="Login to Google", command=google_login, bootstyle="primary")
  1205. login_button.grid(row=1, column=0, padx=20, pady=(10, 10), sticky="w")
  1206.  
  1207. # Task entry frame
  1208. task_notes_frame = ttk.Frame(root)
  1209. task_notes_frame.grid(row=1, column=1, columnspan=2, padx=10, pady=(10, 10), sticky="ew")
  1210.  
  1211. task_label = ttk.Label(task_notes_frame, text="Task:", bootstyle="info")
  1212. task_label.grid(row=0, column=0, padx=(10, 0), pady=(10, 0), sticky="e")
  1213.  
  1214. task_entry = ttk.Entry(task_notes_frame, width=50)
  1215. task_entry.grid(row=0, column=1, padx=(5, 10), pady=(10, 0), sticky="ew")
  1216.  
  1217. notes_label = ttk.Label(task_notes_frame, text="Notes:", bootstyle="info")
  1218. notes_label.grid(row=1, column=0, padx=(10, 0), pady=(5, 0), sticky="ne")
  1219.  
  1220. notes_entry = scrolledtext.ScrolledText(task_notes_frame, height=5, width=50)
  1221. notes_entry.grid(row=1, column=1, padx=(5, 10), pady=(5, 5), sticky="ew")
  1222.  
  1223. # Create the at_date_frame and by_date_frame globally
  1224. global at_date_frame, by_date_frame
  1225.  
  1226. # Create the central_frame (Complete at/by section)
  1227. central_frame = ttk.Frame(root)
  1228. central_frame.grid(row=2, column=1, columnspan=2, padx=10, pady=10, sticky="n")
  1229.  
  1230. # "Complete at" date selection
  1231. at_date_frame, at_month_combo, at_day_combo, at_year_combo = create_date_selection_frame(central_frame)
  1232. at_date_frame.grid(row=0, column=4, padx=(5, 5), pady=0, sticky="w")
  1233. at_date_frame.grid_remove()  # Initially hidden until the checkbox is selected
  1234.  
  1235. # "Complete by" date selection
  1236. by_date_frame, by_month_combo, by_day_combo, by_year_combo = create_date_selection_frame(central_frame)
  1237. by_date_frame.grid(row=1, column=4, padx=(5, 5), pady=0, sticky="w")
  1238. by_date_frame.grid_remove()  # Initially hidden until the checkbox is selected
  1239.  
  1240. # "Complete at" time selection and date
  1241. at_radio_button = ttk.Checkbutton(central_frame, text="Complete at", variable=at_radio_var, command=update_state, bootstyle="primary-round-toggle")
  1242. at_radio_button.grid(row=0, column=0, padx=(5, 5), pady=0, sticky="w")
  1243.  
  1244. at_time_frame = create_time_selection_frame(central_frame, "12", "00", "AM", is_at=True)
  1245. at_time_frame.grid(row=0, column=1, padx=(5, 5), pady=0, sticky="w")
  1246.  
  1247. 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")
  1248. at_date_checkbox.grid(row=0, column=3, padx=(5, 5), pady=0, sticky="w")
  1249.  
  1250. # "Complete by" time selection and date
  1251. by_radio_button = ttk.Checkbutton(central_frame, text="Complete by", variable=by_radio_var, command=update_state, bootstyle="primary-round-toggle")
  1252. by_radio_button.grid(row=1, column=0, padx=(5, 5), pady=0, sticky="w")
  1253.  
  1254. by_time_frame = create_time_selection_frame(central_frame, "12", "00", "AM", is_at=False)
  1255. by_time_frame.grid(row=1, column=1, padx=(5, 5), pady=0, sticky="w")
  1256.  
  1257. 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")
  1258. by_date_checkbox.grid(row=1, column=3, padx=(5, 5), pady=0, sticky="w")
  1259.  
  1260. # Create the separator line between Complete at/by and Recurrence options
  1261. separator = ttk.Separator(root, orient='horizontal')
  1262. separator.grid(row=3, column=0, columnspan=4, sticky="ew", padx=10, pady=10)
  1263.  
  1264. # Recurrence checkboxes and day selectors
  1265. daily_var = ttk.IntVar()
  1266. weekly_var = ttk.IntVar()
  1267. monthly_var = ttk.IntVar()
  1268.  
  1269. # Frame for Recurrence settings
  1270. recurrence_frame = ttk.Frame(root)
  1271. recurrence_frame.grid(row=4, column=1, columnspan=2, padx=5, pady=5, sticky="n")
  1272.  
  1273. # Daily recurrence
  1274. daily_checkbox = ttk.Checkbutton(recurrence_frame, text="Daily", variable=daily_var, command=update_state, bootstyle="primary-round-toggle")
  1275. daily_checkbox.grid(row=0, column=0, padx=5, pady=5, sticky="w")
  1276.  
  1277. daily_hour_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 13)], width=3, bootstyle="superhero")
  1278. daily_hour_combo.set("12")
  1279. daily_hour_combo.grid(row=0, column=1, padx=(2, 2), pady=5, sticky="e")
  1280.  
  1281. daily_minute_combo = ttk.Combobox(recurrence_frame, values=[f"{i:02}" for i in range(0, 60)], width=3)
  1282. daily_minute_combo.set("00")
  1283. daily_minute_combo.grid(row=0, column=2, padx=(2, 2), pady=5, sticky="w")
  1284.  
  1285. daily_am_pm_combo = ttk.Combobox(recurrence_frame, values=["AM", "PM"], width=3)
  1286. daily_am_pm_combo.set("AM")
  1287. daily_am_pm_combo.grid(row=0, column=3, padx=(2, 5), pady=5, sticky="w")
  1288.  
  1289. # Weekly recurrence
  1290. weekly_checkbox = ttk.Checkbutton(recurrence_frame, text="Weekly", variable=weekly_var, command=update_state, bootstyle="primary-round-toggle")
  1291. weekly_checkbox.grid(row=1, column=0, padx=5, pady=5, sticky="w")
  1292.  
  1293. weekly_day_combo = ttk.Combobox(recurrence_frame, values=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], width=10)
  1294. weekly_day_combo.set("Monday")
  1295. weekly_day_combo.grid(row=1, column=1, padx=(2, 2), pady=5, sticky="w")
  1296.  
  1297. weekly_hour_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 13)], width=3)
  1298. weekly_hour_combo.set("12")
  1299. weekly_hour_combo.grid(row=1, column=2, padx=(2, 2), pady=5, sticky="w")
  1300.  
  1301. weekly_minute_combo = ttk.Combobox(recurrence_frame, values=[f"{i:02}" for i in range(0, 60)], width=3)
  1302. weekly_minute_combo.set("00")
  1303. weekly_minute_combo.grid(row=1, column=3, padx=(2, 2), pady=5, sticky="w")
  1304.  
  1305. weekly_am_pm_combo = ttk.Combobox(recurrence_frame, values=["AM", "PM"], width=3)
  1306. weekly_am_pm_combo.set("AM")
  1307. weekly_am_pm_combo.grid(row=1, column=4, padx=(2, 5), pady=5, sticky="w")
  1308.  
  1309. # Monthly recurrence
  1310. monthly_checkbox = ttk.Checkbutton(recurrence_frame, text="Monthly", variable=monthly_var, command=update_state, bootstyle="primary-round-toggle")
  1311. monthly_checkbox.grid(row=2, column=0, padx=5, pady=5, sticky="w")
  1312.  
  1313. monthly_day_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 32)], width=5)
  1314. monthly_day_combo.set("1")
  1315. monthly_day_combo.grid(row=2, column=1, padx=(2, 2), pady=5, sticky="e")
  1316.  
  1317. monthly_hour_combo = ttk.Combobox(recurrence_frame, values=[str(i) for i in range(1, 13)], width=3)
  1318. monthly_hour_combo.set("12")
  1319. monthly_hour_combo.grid(row=2, column=2, padx=(2, 2), pady=5, sticky="w")
  1320.  
  1321. monthly_minute_combo = ttk.Combobox(recurrence_frame, values=[f"{i:02}" for i in range(0, 60)], width=3)
  1322. monthly_minute_combo.set("00")
  1323. monthly_minute_combo.grid(row=2, column=3, padx=(2, 2), pady=5, sticky="w")
  1324.  
  1325. monthly_am_pm_combo = ttk.Combobox(recurrence_frame, values=["AM", "PM"], width=3)
  1326. monthly_am_pm_combo.set("AM")
  1327. monthly_am_pm_combo.grid(row=2, column=4, padx=(2, 5), pady=5, sticky="w")
  1328.  
  1329. # Task list frame with scrollbars
  1330. task_frame = ttk.Frame(root)
  1331. task_frame.grid(row=5, column=0, columnspan=4, padx=10, pady=10, sticky="nsew")
  1332.  
  1333. # Labels for task types
  1334. daily_label = ttk.Label(task_frame, text="Daily Tasks", bootstyle="info")
  1335. daily_label.grid(row=0, column=0, padx=5, pady=5)
  1336.  
  1337. weekly_label = ttk.Label(task_frame, text="Weekly Tasks", bootstyle="info")
  1338. weekly_label.grid(row=0, column=1, padx=5, pady=5)
  1339.  
  1340. monthly_label = ttk.Label(task_frame, text="Monthly Tasks", bootstyle="info")
  1341. monthly_label.grid(row=0, column=2, padx=5, pady=5)
  1342.  
  1343. main_label = ttk.Label(task_frame, text="Main Tasks (Complete at/by)", bootstyle="info")
  1344. main_label.grid(row=0, column=3, padx=5, pady=5)
  1345.  
  1346. # Create task listboxes
  1347. daily_task_listbox = tk.Listbox(task_frame, height=25, width=70)
  1348. daily_task_listbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
  1349. daily_scrollbar = ttk.Scrollbar(task_frame, orient="vertical")
  1350. daily_scrollbar.grid(row=1, column=0, sticky="nse")
  1351. daily_task_listbox.config(yscrollcommand=daily_scrollbar.set)
  1352. daily_scrollbar.config(command=daily_task_listbox.yview)
  1353.  
  1354. weekly_task_listbox = tk.Listbox(task_frame, height=25, width=70)
  1355. weekly_task_listbox.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
  1356. weekly_scrollbar = ttk.Scrollbar(task_frame, orient="vertical")
  1357. weekly_scrollbar.grid(row=1, column=1, sticky="nse")
  1358. weekly_task_listbox.config(yscrollcommand=weekly_scrollbar.set)
  1359. weekly_scrollbar.config(command=weekly_task_listbox.yview)
  1360.  
  1361. monthly_task_listbox = tk.Listbox(task_frame, height=25, width=70)
  1362. monthly_task_listbox.grid(row=1, column=2, padx=5, pady=5, sticky="nsew")
  1363. monthly_scrollbar = ttk.Scrollbar(task_frame, orient="vertical")
  1364. monthly_scrollbar.grid(row=1, column=2, sticky="nse")
  1365. monthly_task_listbox.config(yscrollcommand=monthly_scrollbar.set)
  1366. monthly_scrollbar.config(command=monthly_task_listbox.yview)
  1367.  
  1368. task_listbox = tk.Listbox(task_frame, height=25, width=90)
  1369. task_listbox.grid(row=1, column=3, padx=5, pady=5, sticky="nsew")
  1370. main_scrollbar = ttk.Scrollbar(task_frame, orient="vertical")
  1371. main_scrollbar.grid(row=1, column=3, sticky="nse")
  1372. task_listbox.config(yscrollcommand=main_scrollbar.set)
  1373. main_scrollbar.config(command=task_listbox.yview)
  1374.  
  1375. # Button to add tasks
  1376. button_frame = ttk.Frame(root)
  1377. button_frame.grid(row=6, column=0, columnspan=4)
  1378.  
  1379. add_task_button = ttk.Button(button_frame, text="Add Task", command=add_task, bootstyle="success")
  1380. add_task_button.grid(row=0, column=0, padx=5, pady=10)
  1381.  
  1382. mark_in_progress_button = ttk.Button(button_frame, text="Mark In-Progress", command=mark_task_in_progress, bootstyle="warning")
  1383. mark_in_progress_button.grid(row=0, column=1, padx=5, pady=10)
  1384.  
  1385. mark_completed_button = ttk.Button(button_frame, text="Mark Completed", command=mark_task_completed, bootstyle="succcess")
  1386. mark_completed_button.grid(row=0, column=2, padx=5, pady=10)
  1387.  
  1388. remove_task_button = ttk.Button(button_frame, text="Remove Task", command=remove_task, bootstyle="danger")
  1389. remove_task_button.grid(row=0, column=3, padx=5, pady=10)
  1390.  
  1391. # Bind double-click event to listboxes to show task notes
  1392. task_listbox.bind("<Double-Button-1>", show_notes)
  1393. daily_task_listbox.bind("<Double-Button-1>", show_notes)
  1394. weekly_task_listbox.bind("<Double-Button-1>", show_notes)
  1395. monthly_task_listbox.bind("<Double-Button-1>", show_notes)
  1396.  
  1397. # Bind the click event on the root window to clear task selection
  1398. root.bind("<Button-1>", clear_all_selections)
  1399.  
  1400. # Allow natural selection behavior in listboxes
  1401. task_listbox.bind("<Button-1>", lambda e: task_listbox.selection_anchor(task_listbox.nearest(e.y)))
  1402. daily_task_listbox.bind("<Button-1>", lambda e: daily_task_listbox.selection_anchor(daily_task_listbox.nearest(e.y)))
  1403. weekly_task_listbox.bind("<Button-1>", lambda e: weekly_task_listbox.selection_anchor(weekly_task_listbox.nearest(e.y)))
  1404. monthly_task_listbox.bind("<Button-1>", lambda e: monthly_task_listbox.selection_anchor(monthly_task_listbox.nearest(e.y)))
  1405.  
  1406. # Call this function right after creating all the widgets to set the initial state
  1407. initialize_state()
  1408.  
  1409. # Call the login check on startup in a separate thread
  1410. check_login_status_in_background()
  1411.  
  1412. # Load tasks on startup
  1413. load_tasks()
  1414.  
  1415. # Start the Tkinter event loop
  1416. start_time_checker()
  1417. root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement