diff --git a/SMTplugins/Calendar/calendarWidget.py b/SMTplugins/Calendar/calendarWidget.py index 2744cf6..29eee92 100644 --- a/SMTplugins/Calendar/calendarWidget.py +++ b/SMTplugins/Calendar/calendarWidget.py @@ -1,17 +1,33 @@ -# calendarWidget.py -# Displays a monthly calendar with Google Calendar event integration (optional) -# Falls back to a static calendar view if no credentials are provided +# SMTplugins/Calendar/calendarWidget.py from widget import Widget -from datetime import datetime, date +from datetime import datetime, date, timedelta import calendar import json +import os +import time + +# Google API Imports +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build + +# Calendar Read-Only Scope +SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] class calendarWidget(Widget): def __init__(self): self._preferences = self.widgetDefaultPreferences - self._events = {} # date_str -> list of event names + self.file = "calendar_events.json" + self._events = {} + self.load_local_events() + + def load_local_events(self): + if os.path.exists(self.file): + with open(self.file, "r") as f: + self._events = json.load(f) @property def widgetName(self): @@ -23,17 +39,15 @@ def widgetID(self): @property def widgetHTML(self): - """Returns the HTML template name; Flask renders it via Jinja.""" return "calendar_widget.html" @property def widgetData(self): - """Returns current calendar data as a JSON-serialisable dict.""" today = date.today() year = today.year month = today.month - cal = calendar.Calendar(firstweekday=6) # week starts Sunday + cal = calendar.Calendar(firstweekday=6) weeks = cal.monthdatescalendar(year, month) weeks_data = [] @@ -58,66 +72,145 @@ def widgetData(self): @property def widgetPreferences(self): - return self._preferences - - @widgetPreferences.setter - def widgetPreferences(self, value): - self._preferences = value + return {} + @property def widgetDefaultPreferences(self): return { "show_week_numbers": False, - "use_google_cal": False, # Set True + provide creds to enable + "use_google_cal": True, # Enabled by default for sync "google_cal_id": "primary" } - @property - def updateTimer(self): - # Refresh every 10 minutes - return 600_000 + def get_service(self): + """Authenticated Google Calendar service helper.""" + creds = None + # Paths consistent with SMT structure + token_path = os.path.join("SMTplugins", "Calendar", "token_cal.json") + creds_path = os.path.join("SMTplugins", "Calendar", "credentials.json") + + if os.path.exists(token_path): + creds = Credentials.from_authorized_user_file(token_path, SCOPES) + + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(creds_path, SCOPES) + creds = flow.run_local_server(port=0) + with open(token_path, 'w') as token: + token.write(creds.to_json()) + + return build('calendar', 'v3', credentials=creds) def update(self): - """Called by the widget subsystem on a timer. Fetches events if enabled.""" + """Called by the subsystem to refresh data.""" if self._preferences.get("use_google_cal"): self._fetch_google_events() else: - # Placeholder: inject a couple of demo events so the UI isn't empty - today = date.today().isoformat() - self._events = { - today: ["Class 4PM – LC 22", "Gym 5PM"] - } - print(f"Calendar Widget updated – {date.today().strftime('%B %Y')}") + if not self._events: + # Default demo data + today = date.today().isoformat() + self._events[today] = [{"id": "demo1", "title": "Class 4PM – LC 22"}] def _fetch_google_events(self): - """ - Stub for Google Calendar API integration. - To enable: install google-auth + google-api-python-client, - create OAuth credentials, and fill in the logic below. - """ + """Fetches events for the current month and syncs to self._events.""" try: - # TODO: implement OAuth flow and fetch events for current month - # from googleapiclient.discovery import build - # service = build("calendar", "v3", credentials=creds) - # events_result = service.events().list(...).execute() - raise NotImplementedError("Google Calendar auth not yet configured.") + service = self.get_service() + now = datetime.utcnow() + # Range: Start of current month to end of month + start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0).isoformat() + 'Z' + + events_result = service.events().list( + calendarId=self._preferences.get("google_cal_id", "primary"), + timeMin=start_date, + maxResults=100, + singleEvents=True, + orderBy='startTime' + ).execute() + + google_events = events_result.get('items', []) + + # Temporary dict to avoid mixing old local events with fresh Google data + new_events_map = {} + + for event in google_events: + start = event['start'].get('dateTime', event['start'].get('date')) + # Extract just the YYYY-MM-DD part + date_key = start[:10] + + if date_key not in new_events_map: + new_events_map[date_key] = [] + + new_events_map[date_key].append({ + "id": event['id'], + "title": event.get('summary', '(No Title)'), + "google_event": True + }) + + self._events = new_events_map + # Persist to disk + with open(self.file, "w") as f: + json.dump(self._events, f) + except Exception as e: - print(f"[calendarWidget] Google Calendar fetch failed: {e}") + print(f"[calendarWidget] Google Calendar sync failed: {e}") + + def updateTimer(self): + return 60000 # Refresh every 1 minute to stay within API limits def handle_event(self, event, args): - """ - Supported events: - - "prev_month" : (future) navigate to previous month - - "next_month" : (future) navigate to next month - - "add_event" : args = {"date": "YYYY-MM-DD", "title": "..."} - """ + + # -------- ADD TASK -------- if event == "add_event": date_str = args.get("date") title = args.get("title", "Event") + if date_str: if date_str not in self._events: self._events[date_str] = [] - self._events[date_str].append(title) - print(f"[calendarWidget] Added event '{title}' on {date_str}") + + self._events[date_str].append({ + "id": int(time.time()*1000), + "title": title + }) + + self._save_events() + + # -------- NEXT MONTH -------- + elif event == "next_month": + print("NEXT MONTH TRIGGERED") + + if self.current_date.month == 12: + self.current_date = self.current_date.replace( + year=self.current_date.year + 1, + month=1 + ) + else: + self.current_date = self.current_date.replace( + month=self.current_date.month + 1 + ) + + self._save_state() + print("NEW MONTH:", self.current_date) + + # -------- PREVIOUS MONTH -------- + elif event == "prev_month": + print("PREV MONTH TRIGGERED") + + if self.current_date.month == 1: + self.current_date = self.current_date.replace( + year=self.current_date.year - 1, + month=12 + ) + else: + self.current_date = self.current_date.replace( + month=self.current_date.month - 1 + ) + + self._save_state() + print("NEW MONTH:", self.current_date) + else: print(f"[calendarWidget] Unhandled event: {event} args={args}") \ No newline at end of file diff --git a/SMTplugins/Calendar/credentials.json b/SMTplugins/Calendar/credentials.json new file mode 100644 index 0000000..d877c52 --- /dev/null +++ b/SMTplugins/Calendar/credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"772172144197-ldo19j0f7s5m7s55jd6ndkadoj4c4ehd.apps.googleusercontent.com","project_id":"smt-tasks","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost"]}} \ No newline at end of file diff --git a/SMTplugins/Calendar/token_cal.json b/SMTplugins/Calendar/token_cal.json new file mode 100644 index 0000000..acb34eb --- /dev/null +++ b/SMTplugins/Calendar/token_cal.json @@ -0,0 +1 @@ +{"token": "ya29.a0AQvPyINm3-Umt-poLbkfSpf3sLXjFOYawOQnHssKmcD6iWd6qI8Vxog2HqhWPjDc4-Gwib_qHJB0JgbnWlVCZeoMCrkxipNogHWpkAVsD4rjQxllKDdjlO7lcHQiFEODBc05EtXZHSdpdkqp3ah9cWzQeOHi2QksUPudqv8QNapkJpvElGqKDljOyvnuJMENZ8bTXnEaCgYKAaMSARQSFQHGX2Mi1bah18Yfs0QmPf9s0gUP6A0206", "refresh_token": "1//05NlvPY06LAVrCgYIARAAGAUSNwF-L9IrBh5LQKCmxDpM-lbVWh3B_Grots1N2s4FdVmAsdTZQ3AeBEwIUmtCI3uZRvfDXKTb4V4", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "772172144197-ldo19j0f7s5m7s55jd6ndkadoj4c4ehd.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/calendar.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2026-04-30T16:24:43Z"} \ No newline at end of file diff --git a/calendar_events.json b/calendar_events.json new file mode 100644 index 0000000..c5877dd --- /dev/null +++ b/calendar_events.json @@ -0,0 +1 @@ +{"2026-05-24": [{"id": "8ifd00fvn8hqjf73o7o24he5os", "title": "Photography Session (Easha Mashud)", "google_event": true}]} \ No newline at end of file diff --git a/flaskServer.py b/flaskServer.py index 77d379e..a27a1a0 100644 --- a/flaskServer.py +++ b/flaskServer.py @@ -29,7 +29,7 @@ #allows for easier starting of flask from start file def run_flask(): - app.run(debug=True, port=5000, use_reloader=False) + app.run(debug=True, port=8000, use_reloader=False) diff --git a/layout_client.json b/layout_client.json index 6411891..9bc30f2 100644 --- a/layout_client.json +++ b/layout_client.json @@ -33,10 +33,10 @@ "col": 3 }, { - "id": "bj-container", - "name": "Blackjack", - "class": "blackjack-widget", - "css_name": "blackjack_widget.css", + "id": "calendar", + "name": "Calendar", + "class": "calendar-widget", + "css_name": "calendar_widget.css", "row": 2, "col": 2 }, diff --git a/static/js/plugins/calendar_script.js b/static/js/plugins/calendar_script.js index 60e78db..4f96e15 100644 --- a/static/js/plugins/calendar_script.js +++ b/static/js/plugins/calendar_script.js @@ -1,15 +1,220 @@ +/** + * PREREQUISITES FOR THIS FILE: + * 1. An HTML container with id="calendar". + * 2. An API endpoint at /widget/calendar that returns HTML. + * 3. An API endpoint at /api/calendar/event that handles POST requests. + * 4. A global window.calendarEvents array (usually provided by the server-side HTML). + */ + +let selectedDate = null; +let calendarInitialized = false; + +/** + * Attaches event listeners to the calendar container. + * Uses delegation so we don't have to re-bind listeners to every single day. + */ +function attachCalendarListeners() { + const container = document.getElementById("calendar"); + if (!container) return; + + calendarInitialized = true; + + container.addEventListener("click", (e) => { + const day = e.target.closest(".cal-day"); + if (!day) return; + + selectedDate = day.dataset.date; + + // UI Elements + const eventsContainer = document.getElementById("selectedEventsContainer"); + const title = document.getElementById("eventsTitle"); + const taskSection = document.getElementById("taskSection"); + const label = document.getElementById("selectedDateLabel"); + + if (eventsContainer && title && window.calendarEvents) { + let events = []; + + // Find events for the clicked date from the global data store + window.calendarEvents.forEach(week => { + week.forEach(dayData => { + if (dayData.date_str === selectedDate) { + events = dayData.events; + } + }); + }); + + title.innerText = new Date(selectedDate).toDateString(); + eventsContainer.innerHTML = ""; + + if (events.length > 0) { + events.forEach(e => { + const div = document.createElement("div"); + div.className = "cal-event-item"; + div.style.display = "flex"; + div.style.marginBottom = "5px"; + div.innerHTML = ` + ${e.title || e} +