// ==UserScript== // @name UBC Workday Calendar Generator // @namespace http://tampermonkey.net/ // @version 1.2 // @description Adds a 'Download Calendar (.ics)' button to the Workday popup list and generates ICS file on click // @match *.myworkday.com/ubc/* // @author TU // @license TU // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; function formatToICS(event, courseNameCal, courseTypeCal, uniqueId) { const crlf = '\r\n'; function convertTime(time) { const match = time.match(/(\d+):(\d+)\s*(a\.m\.|p\.m\.)/i); if (!match) return ''; // If time format is invalid, return empty string let [_, hours, minutes, period] = match; let hours24 = parseInt(hours, 10); if (period.toLowerCase() === 'p.m.' && hours24 !== 12) { hours24 += 12; } else if (period.toLowerCase() === 'a.m.' && hours24 === 12) { hours24 = 0; } return `${hours24.toString().padStart(2, '0')}${minutes.padStart(2, '0')}00`; } const startTime = convertTime(event.startTime); const endTime = convertTime(event.endTime); const dtstamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; // Ensure unique UID for each event const uid = `${uniqueId}-${event.date}@tupreti.com`; // Construct the ICS event with CRLF line endings const icsEvent = `BEGIN:VEVENT${crlf}` + `UID:${uid}${crlf}` + `DTSTAMP:${dtstamp}${crlf}` + `DTSTART:${event.date}T${startTime}${crlf}` + `DTEND:${event.date}T${endTime}${crlf}` + `SUMMARY:${courseNameCal || 'No Title'}${crlf}` + `DESCRIPTION:${courseTypeCal || 'No Description'}${crlf}` + `LOCATION:${event.location || 'No Location'}${crlf}` + `END:VEVENT`; return icsEvent; } function generateICSFile(events) { const crlf = '\r\n'; const icsContent = `BEGIN:VCALENDAR${crlf}` + `VERSION:2.0${crlf}` + `PRODID:-//Tanish Upreti//UBC Workday Calendar Generator//EN${crlf}` + events.join(crlf) + // Join events without extra space crlf + `END:VCALENDAR`; const blob = new Blob([icsContent], { type: 'text/calendar' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'UBC Course Schedule.ics'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function processMeetingPattern(meetingPattern) { let [dateRange, daysAndWeeks, timeRange, location] = meetingPattern.split(" | "); let [startDate, endDate] = dateRange.split(" - "); let alternateWeeks = daysAndWeeks.includes("(Alternate weeks)"); let days = alternateWeeks ? daysAndWeeks.replace("(Alternate weeks)", "").trim() : daysAndWeeks.trim(); let [startTime, endTime] = timeRange.split(" - "); // Convert dates to format YYYYMMDD startDate = startDate.replace(/-/g, ''); endDate = endDate.replace(/-/g, ''); return { startDate, endDate, days, alternateWeeks, startTime, endTime, location }; } function getDatesBetween(startDate, endDate, daysOfWeek, alternateWeeks = false) { const start = new Date(startDate.slice(0, 4), startDate.slice(4, 6) - 1, startDate.slice(6, 8)); const end = new Date(endDate.slice(0, 4), endDate.slice(4, 6) - 1, endDate.slice(6, 8)); const dayMap = { 'Mon': 1, 'Tue': 2, 'Wed': 3, 'Thu': 4, 'Fri': 5, 'Sat': 6, 'Sun': 0 }; const days = daysOfWeek.split(' ').map(day => dayMap[day]); const dates = []; let weekCount = 0; for (let d = start; d <= end; d.setDate(d.getDate() + 1)) { if (days.includes(d.getDay())) { // If it's alternate weeks, only push dates every other week if (!alternateWeeks || (alternateWeeks && weekCount % 2 === 0)) { dates.push(new Date(d).toISOString().slice(0, 10).replace(/-/g, '')); } } if (d.getDay() === 0) { weekCount++; } } return dates; } function extractAllCourseNamesAndGenerateICS() { console.log("ICS generation triggered"); // Use requestIdleCallback for improved performance requestIdleCallback(() => { try { const divs = document.querySelectorAll('div[data-automation-label]'); const courseNames = []; divs.forEach(div => { const courseName = div.getAttribute('data-automation-label'); if (courseName) { courseNames.push(courseName); } }); const pattern = /\b[A-Z]{3,4}_O \d{3}-[A-Z0-9]{1,4} - .+/; const events = []; for (let i = 0; i < courseNames.length; i++) { const currentItem = courseNames[i]; if (pattern.test(currentItem)) { const course = currentItem; const instructionalFormat = courseNames[i + 1]; // Collect all meeting patterns for this course let j = i + 3; while (j < courseNames.length && courseNames[j].includes('|')) { const meetingPatterns = courseNames[j]; const parsed = processMeetingPattern(meetingPatterns); const occurrenceDates = getDatesBetween(parsed.startDate, parsed.endDate, parsed.days, parsed.alternateWeeks); occurrenceDates.forEach((date, index) => { const event = formatToICS({ ...parsed, date }, course, instructionalFormat, i + '-' + index); events.push(event); }); j++; } // Skip processed patterns i = j - 1; } } if (events.length > 0) { generateICSFile(events); } else { console.log("No events found to generate."); } } catch (error) { console.error("Error while extracting course names and generating ICS:", error); } }); } function addCalendarDownloadButton() { const selectedTab = document.querySelector('li[data-automation-id="selectedTab"]'); if (selectedTab && selectedTab.querySelector('div[data-automation-id="tabLabel"]').textContent.trim() === 'Registration & Courses') { const popups = document.querySelectorAll('div[data-automation-id="workletPopup"]'); popups.forEach(popup => { const menuList = popup.querySelector('ul[data-automation-id="menuList"]'); if (menuList && !menuList.querySelector('div[data-automation-id="calendarDownloadButton"]')) { console.log("Menu list detected, adding 'Download Calendar (.ics)' button."); // Find an existing list item to copy its classes and structure const existingListItem = menuList.querySelector('li'); if (existingListItem) { const newListItem = existingListItem.cloneNode(true); const newButtonDiv = newListItem.querySelector('div'); // Update the cloned div with the new button's properties newButtonDiv.className = 'WGTQ WCTQ WHSQ WPTQ WNTQ'; // Copy the same class structure newButtonDiv.setAttribute('aria-disabled', 'false'); newButtonDiv.setAttribute('tabindex', '-2'); newButtonDiv.setAttribute('data-automation-id', 'calendarDownloadButton'); newButtonDiv.setAttribute('role', 'option'); newButtonDiv.setAttribute('aria-setsize', (menuList.children.length + 1).toString()); // Adjust size dynamically newButtonDiv.setAttribute('aria-posinset', (menuList.children.length + 1).toString()); // Position at the end newButtonDiv.textContent = 'Download Calendar (.ics)'; newButtonDiv.addEventListener('click', extractAllCourseNamesAndGenerateICS); menuList.appendChild(newListItem); } } }); } } // Initial button setup addCalendarDownloadButton(); // Set up MutationObserver to add the button when the popup is loaded or updated const observer = new MutationObserver(() => { addCalendarDownloadButton(); }); // Observe the body for changes observer.observe(document.body, { childList: true, subtree: true }); })();