您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a 'Download Calendar (.ics)' button to Workday and generates ICS with proper location/description
// ==UserScript== // @name UBC Workday Calendar Generator // @namespace http://tampermonkey.net/ // @version 1.9 // @description Adds a 'Download Calendar (.ics)' button to Workday and generates ICS with proper location/description // @match *://*.myworkday.com/ubc* // @author TU // @license TU // @grant none // ==/UserScript== (function() { 'use strict'; const timeZone = 'America/Vancouver'; function formatToICS(event, courseNameCal, courseTypeCal, uniqueId) { const crlf = '\r\n'; function convertTime(time) { if (!time || typeof time !== 'string') { console.warn("Invalid time input:", time); return '000000'; } const match = time.match(/(\d+):(\d+)\s*(a\.m\.|p\.m\.)/i); if (!match) { console.warn("Time parsing failed:", time); return '000000'; } let [_, hours, minutes, period] = match; let hours24 = parseInt(hours, 10); if (isNaN(hours24)) hours24 = 0; if (period.toLowerCase() === 'p.m.' && hours24 !== 12) hours24 += 12; if (period.toLowerCase() === 'a.m.' && hours24 === 12) hours24 = 0; return `${hours24.toString().padStart(2, '0')}${minutes.padStart(2, '0')}00`; } if (!event || !event.startDate || !event.endDate) { console.error("Invalid event object:", event); return ''; } const startTime = convertTime(event.startTime); const endTime = convertTime(event.endTime); const dtstamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; const uid = `${uniqueId || 'X'}-${event.startDate}@tupreti.com`; const startDate = event.startDate.replace(/-/g, ''); const dayMap = { Mon: 'MO', Tue: 'TU', Wed: 'WE', Thu: 'TH', Fri: 'FR', Sat: 'SA', Sun: 'SU' }; let days = ''; if (event.days) { days = event.days.split(' ') .map(day => dayMap[day] || '') .filter(Boolean) .join(','); } const frequency = days ? `FREQ=WEEKLY;BYDAY=${days}` : `FREQ=WEEKLY`; const until = event.endDate ? `${event.endDate.replace(/-/g, '')}T235959Z` : ''; const recurrenceRule = until ? (event.alternateWeeks ? `${frequency};INTERVAL=2;UNTIL=${until}` : `${frequency};UNTIL=${until}`) : frequency; const icsEvent = `BEGIN:VEVENT${crlf}` + `UID:${uid}${crlf}` + `DTSTAMP:${dtstamp}${crlf}` + (startDate && startTime ? `DTSTART;TZID=${timeZone}:${startDate}T${startTime}${crlf}` : '') + (startDate && endTime ? `DTEND;TZID=${timeZone}:${startDate}T${endTime}${crlf}` : '') + (recurrenceRule ? `RRULE:${recurrenceRule}${crlf}` : '') + `SUMMARY:${courseNameCal || 'No Title'}${crlf}` + `DESCRIPTION:${courseTypeCal || ''}${crlf}` + `LOCATION:${event.location || 'No Location'}${crlf}` + `END:VEVENT`; return icsEvent; } function generateICSFile(events) { if (!Array.isArray(events) || events.length === 0) { console.warn("No events passed to generateICSFile"); return; } const crlf = '\r\n'; const icsContent = `BEGIN:VCALENDAR${crlf}` + `VERSION:2.0${crlf}` + `PRODID:-//Tanish Upreti//UBC Workday Calendar Generator//EN${crlf}` + `CALSCALE:GREGORIAN${crlf}` + `METHOD:PUBLISH${crlf}` + `X-WR-TIMEZONE:${timeZone}${crlf}` + events.join(crlf) + crlf + `END:VCALENDAR`; try { 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); } catch (err) { console.error("Failed to generate ICS file:", err); } } function processMeetingPattern(meetingPattern) { if (!meetingPattern || typeof meetingPattern !== 'string') { console.warn("Invalid meeting pattern:", meetingPattern); return { startDate: '', endDate: '', days: '', alternateWeeks: false, startTime: '', endTime: '', location: '' }; } const parts = meetingPattern.split(" | "); if (parts.length < 3) { console.warn("Unexpected meeting pattern format:", meetingPattern); return { startDate: '', endDate: '', days: '', alternateWeeks: false, startTime: '', endTime: '', location: '' }; } let [dateRange, daysAndWeeks, timeRange, ...locationParts] = parts; let location = locationParts.join(" | ") || 'No Location'; let startDate = '', endDate = ''; if (dateRange.includes(" - ")) { [startDate, endDate] = dateRange.split(" - "); } let alternateWeeks = daysAndWeeks.includes("(Alternate weeks)"); let days = alternateWeeks ? daysAndWeeks.replace("(Alternate weeks)", "").trim() : daysAndWeeks.trim(); let startTime = '', endTime = ''; if (timeRange.includes(" - ")) { [startTime, endTime] = timeRange.split(" - "); } return { startDate, endDate, days, alternateWeeks, startTime, endTime, location }; } function extractUBCO() { const divs = document.querySelectorAll('div[data-automation-label]'); if (!divs.length) { console.info("No UBCO-style divs found."); return []; } const courseNames = []; divs.forEach(div => { const label = div.getAttribute('data-automation-label'); if (label) courseNames.push(label); }); return courseNames; } function extractUBCV() { const rows = document.querySelectorAll("table tbody tr"); if (!rows.length) { console.info("No UBCV-style table rows found."); return []; } const patterns = []; rows.forEach(row => { const cells = row.querySelectorAll("td"); if (cells.length < 5) return; const section = cells[1]?.innerText.trim(); const meetings = cells[4]?.innerText.trim(); const instructionalFormat = cells[2]?.innerText.trim() || ''; if (section && meetings) { const sessions = meetings.split(/\n/).map(s => s.trim()).filter(Boolean); sessions.forEach(session => { patterns.push({ course: section, instructionalFormat, session }); }); } }); return patterns; } function extractAllCourseNamesAndGenerateICS() { console.log("ICS generation triggered"); requestIdleCallback(() => { try { const pattern = /\b[A-Z]{3,4}_[OV]\s\d{3}-[A-Z0-9_]+ - .+/; const events = []; // ---------- UBCO ---------- const courseNames = extractUBCO(); for (let i = 0; i < courseNames.length; i++) { const currentItem = courseNames[i]; if (pattern.test(currentItem)) { const course = currentItem; const instructionalFormat = courseNames[i + 1] || ''; let j = i + 3; while (j < courseNames.length && typeof courseNames[j] === 'string' && courseNames[j].includes('|')) { const parsed = processMeetingPattern(courseNames[j]); const event = formatToICS(parsed, course, instructionalFormat, i); if (event) events.push(event); j++; } i = j - 1; } } // ---------- UBCV ---------- if (events.length === 0) { const ubcvPatterns = extractUBCV(); ubcvPatterns.forEach((item, idx) => { if (!item || !item.course || !pattern.test(item.course)) return; const parsed = processMeetingPattern(item.session); const event = formatToICS(parsed, item.course, item.instructionalFormat, idx); if (event) events.push(event); }); } if (events.length > 0) { generateICSFile(events); console.log("Events generated:", events.length); } else { console.log("No events found to generate."); } } catch (error) { console.error("Error while extracting course names and generating ICS:", error); } }); } function addCalendarDownloadButton() { try { 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."); const existingListItem = menuList.querySelector('li'); if (existingListItem) { const newListItem = existingListItem.cloneNode(true); const newButtonDiv = newListItem.querySelector('div')?.cloneNode(true); if (!newButtonDiv) return; newListItem.replaceChild(newButtonDiv, newListItem.querySelector('div')); newButtonDiv.textContent = 'Download Calendar (.ics)'; newButtonDiv.setAttribute('data-automation-id', 'calendarDownloadButton'); newButtonDiv.setAttribute('aria-label', 'Download Calendar'); newButtonDiv.addEventListener('click', extractAllCourseNamesAndGenerateICS); menuList.appendChild(newListItem); } } }); } } catch (err) { console.error("Error while adding calendar button:", err); } } const observer = new MutationObserver(() => addCalendarDownloadButton()); observer.observe(document, { childList: true, subtree: true }); })(); (function() { 'use strict'; const timeZone = 'America/Vancouver'; 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 ''; let [_, hours, minutes, period] = match; let hours24 = parseInt(hours, 10); if (period.toLowerCase() === 'p.m.' && hours24 !== 12) hours24 += 12; 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'; const uid = `${uniqueId}-${event.startDate}@tupreti.com`; const startDate = event.startDate.replace(/-/g, ''); const dayMap = { Mon: 'MO', Tue: 'TU', Wed: 'WE', Thu: 'TH', Fri: 'FR', Sat: 'SA', Sun: 'SU' }; const days = event.days.split(' ').map(day => dayMap[day]).join(','); const frequency = `FREQ=WEEKLY;BYDAY=${days}`; const recurrenceRule = event.alternateWeeks ? `${frequency};INTERVAL=2;UNTIL=${event.endDate.replace(/-/g, '')}T235959Z` : `${frequency};UNTIL=${event.endDate.replace(/-/g, '')}T235959Z`; const icsEvent = `BEGIN:VEVENT${crlf}` + `UID:${uid}${crlf}` + `DTSTAMP:${dtstamp}${crlf}` + `DTSTART;TZID=${timeZone}:${startDate}T${startTime}${crlf}` + `DTEND;TZID=${timeZone}:${startDate}T${endTime}${crlf}` + `RRULE:${recurrenceRule}${crlf}` + `SUMMARY:${courseNameCal || 'No Title'}${crlf}` + `DESCRIPTION:${courseTypeCal || 'Lecture'}${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}` + `CALSCALE:GREGORIAN${crlf}` + `METHOD:PUBLISH${crlf}` + `X-WR-TIMEZONE:${timeZone}${crlf}` + events.join(crlf) + 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) { const parts = meetingPattern.split(" | "); if (parts.length < 4) return {}; const [dateRange, daysAndWeeks, timeRange] = parts; let locationFull = parts.slice(3).join(" | "); const [startDate, endDate] = dateRange.split(" - "); const alternateWeeks = daysAndWeeks.includes("(Alternate weeks)"); const days = alternateWeeks ? daysAndWeeks.replace("(Alternate weeks)", "").trim() : daysAndWeeks.trim(); const [startTime, endTime] = timeRange.split(" - "); return { startDate, endDate, days, alternateWeeks, startTime, endTime, location: locationFull }; } function extractAllCourseNamesAndGenerateICS() { console.log("ICS generation triggered"); requestIdleCallback(() => { try { const divs = document.querySelectorAll('div[data-automation-label]'); const courseNames = []; divs.forEach(div => { const label = div.getAttribute('data-automation-label'); if (label) courseNames.push(label); }); console.log("Total data-automation-label divs found:", courseNames.length); const pattern = /\b[A-Z]{3,4}_[OV] \d{3}-[A-Z0-9]{1,4} - .+/; const events = []; for (let i = 0; i < courseNames.length; i++) { const currentItem = courseNames[i]; console.log(`Checking courseNames[${i}]:`, currentItem); if (pattern.test(currentItem)) { console.log("Matched course regex:", currentItem); const course = currentItem; const instructionalFormat = courseNames[i + 1] || 'Lecture'; console.log("Instructional format:", instructionalFormat); let j = i + 3; while (j < courseNames.length && courseNames[j].includes('|')) { const meetingPattern = courseNames[j]; console.log("Processing meeting pattern:", meetingPattern); const parsed = processMeetingPattern(meetingPattern); console.log("Parsed meeting pattern:", parsed); if (parsed.startDate) { const event = formatToICS(parsed, course, instructionalFormat, i); events.push(event); } j++; } i = j - 1; } } if (events.length > 0) { console.log("Events generated:", events.length); generateICSFile(events); } else { console.log("No events found to generate. Check regex or meeting pattern format."); } } 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."); const existingListItem = menuList.querySelector('li'); if (existingListItem) { const newListItem = existingListItem.cloneNode(true); const newButtonDiv = newListItem.querySelector('div').cloneNode(true); newListItem.replaceChild(newButtonDiv, newListItem.querySelector('div')); newButtonDiv.textContent = 'Download Calendar (.ics)'; newButtonDiv.setAttribute('data-automation-id', 'calendarDownloadButton'); newButtonDiv.setAttribute('aria-label', 'Download Calendar'); newButtonDiv.addEventListener('click', extractAllCourseNamesAndGenerateICS); menuList.appendChild(newListItem); } } }); } } const observer = new MutationObserver(() => { addCalendarDownloadButton(); }); observer.observe(document, { childList: true, subtree: true }); })();